PostgreSQL入門 — 基本SQL完全ガイド
テーブル・カラム一覧の確認から JOIN・更新系SQL・ビューの違いまで、PostgreSQL初心者が現場で使える知識を一通り学ぶ
PostgreSQLとは
PostgreSQL(ポストグレスQL)は、世界で最も広く使われているオープンソースのリレーショナルデータベース(RDBMS)の一つ。
MySQLと並んでWebアプリのバックエンドで頻繁に採用され、NestJSやRailsとの組み合わせも一般的。
リレーショナルデータベースとは
データをテーブル(行と列の表)で管理し、テーブル同士をリレーション(関係)で結びつける仕組み。
Excelのシートをイメージするとわかりやすい。
基本用語の整理
| 用語 | 意味 | Excelで例えると |
|---|---|---|
| データベース | データの格納場所(最上位の入れ物) | Excelファイル |
| テーブル | 実際にデータが入る表 | シート |
| カラム(列) | テーブルのデータ項目の定義 | 列ヘッダー |
| レコード(行) | テーブルの1件分のデータ | 1行 |
| 主キー(PK) | レコードを一意に特定するカラム | 行番号(重複なし) |
| 外部キー(FK) | 別テーブルの主キーを参照するカラム | 別シートへの参照 |
テーブル・カラム一覧の確認
MySQLでは SHOW TABLES や SHOW COLUMNS を使うが、PostgreSQLでは独自の情報スキーマや psql のバックスラッシュコマンドを使う。
psqlで一覧を確認する(コマンドライン)
# データベース一覧
\l
# 接続先を切り替え
\c mydb
# テーブル一覧
\dt
# 特定テーブルのカラム情報
\d usersSQL でテーブル一覧を確認する
-- 現在のデータベース内のテーブル一覧
SELECT table_name
FROM information_schema.tables
WHERE table_schema = 'public'
AND table_type = 'BASE TABLE'
ORDER BY table_name;information_schema.tables はPostgreSQLが内部で持つメタデータテーブル。
table_schema = 'public' は一般的なデフォルトスキーマを指す。
SQL でカラム一覧を確認する
-- usersテーブルのカラム情報を確認
SELECT
column_name,
data_type,
is_nullable,
column_default
FROM information_schema.columns
WHERE table_name = 'users'
ORDER BY ordinal_position;| column_name | data_type | is_nullable | column_default |
|---|---|---|---|
| id | integer | NO | nextval(...) |
| name | character varying | NO | NULL |
| character varying | NO | NULL | |
| created_at | timestamp | YES | now() |
サンプルテーブルの定義
この記事では以下のテーブルを使って説明する。
-- ユーザーテーブル
CREATE TABLE users (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
email VARCHAR(255) NOT NULL UNIQUE,
created_at TIMESTAMP DEFAULT NOW()
);
-- 注文テーブル
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id),
item_name VARCHAR(200) NOT NULL,
price INTEGER NOT NULL,
ordered_at TIMESTAMP DEFAULT NOW()
);SERIAL は自動採番の整数(PostgreSQL独自の便利な型)。
REFERENCES users(id) が外部キーの定義で、orders.user_id は users.id を参照する。
SELECT — データを取得する
基本形
-- 全カラム・全件取得
SELECT * FROM users;
-- 特定カラムのみ取得
SELECT id, name, email FROM users;* は全カラムを意味するが、本番コードでは必要なカラムだけを明示するのが定石。
WHERE — 条件で絞り込む
-- idが3のユーザーを取得
SELECT * FROM users WHERE id = 3;
-- nameが「田中」を含むユーザー(部分一致)
SELECT * FROM users WHERE name LIKE '%田中%';
-- 複数条件(AND)
SELECT * FROM users
WHERE email LIKE '%@example.com'
AND created_at >= '2026-01-01';
-- 複数条件(OR)
SELECT * FROM users
WHERE name = '田中' OR name = '山田';
-- NULLの確認(= NULLは使えない)
SELECT * FROM orders WHERE user_id IS NULL;NULLの比較は IS NULL / IS NOT NULL を使う。 = NULL は機能しないので注意。
ORDER BY — 並び替え
-- 新しい順(降順)
SELECT * FROM orders ORDER BY ordered_at DESC;
-- 名前のアルファベット順(昇順・デフォルト)
SELECT * FROM users ORDER BY name ASC;
-- 複数条件で並び替え
SELECT * FROM orders ORDER BY price DESC, ordered_at ASC;LIMIT / OFFSET — 件数を制限する
-- 最初の10件だけ取得
SELECT * FROM users ORDER BY id LIMIT 10;
-- 11件目から20件目(ページネーション)
SELECT * FROM users ORDER BY id LIMIT 10 OFFSET 10;OFFSET は「最初の何件をスキップするか」を指定する。
ページネーションAPIではよく使われるパターン。
集計 — GROUP BY と集計関数
-- ユーザーごとの注文数を集計
SELECT
user_id,
COUNT(*) AS order_count,
SUM(price) AS total_price,
AVG(price) AS avg_price
FROM orders
GROUP BY user_id
ORDER BY total_price DESC;| user_id | order_count | total_price | avg_price |
|---|---|---|---|
| 1 | 3 | 15000 | 5000 |
| 2 | 1 | 8000 | 8000 |
主な集計関数:
| 関数 | 意味 |
|---|---|
COUNT(*) | 件数 |
SUM(col) | 合計 |
AVG(col) | 平均 |
MAX(col) | 最大値 |
MIN(col) | 最小値 |
HAVING — 集計後に絞り込む
-- 注文が2件以上のユーザーだけ表示
SELECT user_id, COUNT(*) AS order_count
FROM orders
GROUP BY user_id
HAVING COUNT(*) >= 2;WHERE は集計前、HAVING は集計後のフィルタ。
JOIN — テーブルを結合する
JOINはテーブルを横につなげて、関連データを一度に取得するSQL。
INNER JOIN — 両方に存在するデータだけ取得
SELECT
u.id AS user_id,
u.name,
o.item_name,
o.price
FROM users AS u
INNER JOIN orders AS o ON u.id = o.user_id;ON u.id = o.user_id が結合条件(どのカラムで紐付けるか)。
注文が0件のユーザーは結果に含まれない。
users テーブル orders テーブル
┌────┬──────┐ ┌────┬─────────┬──────────┐
│ id │ name │ │ id │ user_id │item_name │
├────┼──────┤ JOIN ├────┼─────────┼──────────┤
│ 1 │ 田中 │ ◄──────► │ 1 │ 1 │ 商品A │
│ 2 │ 山田 │ ◄──────► │ 2 │ 1 │ 商品B │
│ 3 │ 佐藤 │ (なし) │ 3 │ 2 │ 商品C │
└────┴──────┘ └────┴─────────┴──────────┘
結果:田中・山田のみ(佐藤は注文なしで除外)LEFT JOIN — 左テーブルは全件、右は一致したものだけ
SELECT
u.id AS user_id,
u.name,
o.item_name,
o.price
FROM users AS u
LEFT JOIN orders AS o ON u.id = o.user_id;注文が0件のユーザーも結果に含まれ、orders側のカラムは NULL になる。
| user_id | name | item_name | price |
|---|---|---|---|
| 1 | 田中 | 商品A | 3000 |
| 1 | 田中 | 商品B | 5000 |
| 2 | 山田 | 商品C | 8000 |
| 3 | 佐藤 | NULL | NULL |
注文がない佐藤も行として含まれ、orders側のカラムは NULL になる。
「注文がないユーザーを探す」ときにも使う:
-- 一度も注文したことがないユーザーを抽出
SELECT u.id, u.name
FROM users AS u
LEFT JOIN orders AS o ON u.id = o.user_id
WHERE o.id IS NULL;INNER JOIN vs LEFT JOIN の選び方
| 状況 | 使うJOIN |
|---|---|
| 両方に必ずデータがある前提 | INNER JOIN |
| 片方のデータが存在しない可能性がある | LEFT JOIN |
| 「存在しないレコード」を探したい | LEFT JOIN + WHERE 右.id IS NULL |
3テーブルのJOIN
JOINは連鎖できる:
-- usersとordersとproductsを結合する例(productsテーブルが別にある場合)
SELECT
u.name,
o.ordered_at,
p.product_name,
o.price
FROM users AS u
INNER JOIN orders AS o ON u.id = o.user_id
INNER JOIN products AS p ON o.product_id = p.id;更新系SQL
INSERT — データを追加する
-- 1件追加
INSERT INTO users (name, email)
VALUES ('田中太郎', 'taro@example.com');
-- 複数件まとめて追加
INSERT INTO users (name, email) VALUES
('山田花子', 'hanako@example.com'),
('佐藤次郎', 'jiro@example.com');
-- 追加したIDを返す(PostgreSQL固有)
INSERT INTO users (name, email)
VALUES ('鈴木一郎', 'ichiro@example.com')
RETURNING id, name;RETURNING はPostgreSQLの便利な機能で、INSERT/UPDATE後に値を即座に取得できる。
UPDATE — データを更新する
-- 特定レコードのメールを更新
UPDATE users
SET email = 'new-taro@example.com'
WHERE id = 1;
-- 複数カラムを同時更新
UPDATE users
SET
name = '田中 太郎',
email = 'taro2@example.com'
WHERE id = 1;WHEREなしのUPDATEは全件更新になる。 実行前に必ず確認する。
DELETE — データを削除する
-- 特定レコードを削除
DELETE FROM users WHERE id = 3;
-- 条件に合う複数件を削除
DELETE FROM orders WHERE ordered_at < '2025-01-01';WHEREなしのDELETEは全件削除になる。 本番での実行は特に注意。
テーブル・ビュー・マテリアライズドビューの違い
3つの一言定義:
| 一言で言うと | |
|---|---|
| テーブル | 実データが入っている入れ物 |
| ビュー | 名前を付けたSELECT文(データは持たない) |
| マテリアライズドビュー | SELECT結果を物理保存したコピー(定期的に手動更新が必要) |
テーブル(TABLE)— データの原本
テーブルは実際の値がディスクに書き込まれている唯一の場所。
INSERT / UPDATE / DELETE で変更でき、ビューやマテリアライズドビューは必ずどこかのテーブルを参照している。
-- ordersテーブルに1件追加(実データが書き込まれる)
INSERT INTO orders (user_id, item_name, price) VALUES (1, '商品A', 3000);ビュー(VIEW)— 名前を付けたSELECT文
ビューは SQLクエリそのものを保存したもの で、データは一切保持しない。
CREATE VIEW order_summary AS
SELECT
o.id,
u.name AS user_name,
o.item_name,
o.price
FROM orders AS o
INNER JOIN users AS u ON o.user_id = u.id;SELECT * FROM order_summary を実行すると、PostgreSQLは内部でこれを次のように読み替えて実行する:
-- ビューへのSELECTは、定義したJOINクエリに展開されて実行される
SELECT o.id, u.name AS user_name, o.item_name, o.price
FROM orders AS o
INNER JOIN users AS u ON o.user_id = u.id;つまり ビューを参照するたびに毎回クエリが走る。保存されているのはクエリの定義だけで、結果データではない。
ビューのメリット
複雑なJOINを毎回書かなくてよくなる。
たとえば orders と users を結合した order_summary ビューを作っておけば、どこからでも SELECT * FROM order_summary とだけ書けばよい。
ビューの注意点
テーブルのデータが変われば次にSELECTしたとき自動的に最新値が返る。これはメリットだが、裏を返せば 毎回テーブルを読みに行くコストがかかる。
1000万件のJOINを毎回実行するようなケースだと遅くなる。
マテリアライズドビュー(MATERIALIZED VIEW)— 結果を物理保存したコピー
マテリアライズドビューは、ビューと同じようにSELECT文を定義するが、実行結果をそのままディスクに保存する。
CREATE MATERIALIZED VIEW user_order_stats AS
SELECT
u.id,
u.name,
COUNT(o.id) AS order_count,
SUM(o.price) AS total_price
FROM users AS u
LEFT JOIN orders AS o ON u.id = o.user_id
GROUP BY u.id, u.name;SELECT * FROM user_order_stats を実行しても、元のテーブルは参照しない。
保存済みの結果をそのまま返すだけなので 高速。
更新は自動で行われない
テーブルのデータが変わっても、マテリアライズドビューは古いコピーを返し続ける。
最新化するには明示的に REFRESH を実行する必要がある。
-- このコマンドを実行した瞬間だけ最新データに更新される
REFRESH MATERIALIZED VIEW user_order_stats;バッチ処理やcronで定期実行するのが一般的な使い方。
「データが変わったとき」で比べる
実際の動作の差が一番わかりやすいのは、元テーブルのデータを更新したあと何が起きるか を見ることだ。
-- ordersに新しい注文を1件追加した
INSERT INTO orders (user_id, item_name, price) VALUES (1, '新商品', 9000);この後、それぞれをSELECTするとどうなるか:
| SELECTしたとき | 理由 | |
|---|---|---|
| テーブル | 新しい注文が見える | 直接書き込まれているから |
| ビュー | 新しい注文が見える | 毎回テーブルを読むから |
| マテリアライズドビュー | 見えない(古いまま) | コピーが更新されていないから |
マテリアライズドビューで新しい注文を反映するには REFRESH が必要:
REFRESH MATERIALIZED VIEW user_order_stats;
-- これを実行して初めて新しい注文が集計に含まれるどれをいつ使うか
| 状況 | 使うもの |
|---|---|
| データを保存・更新したい | テーブル 一択 |
| 複雑なJOINをどこからでも簡単に呼び出したい | ビュー(データが小〜中規模なら問題ない) |
| 集計(COUNT/SUM/AVG)が重くて毎回実行すると遅い | マテリアライズドビュー |
| 常に最新データが必要(リアルタイム表示) | ビュー(マテビューは使えない) |
| 1分前のデータでよい(ダッシュボード・レポート) | マテリアライズドビュー |
3つの違いまとめ
| テーブル | ビュー | マテリアライズドビュー | |
|---|---|---|---|
| データの保存 | あり(原本) | なし(クエリ定義のみ) | あり(結果のコピー) |
| 読み取り速度 | 普通 | 遅くなることがある | 高速(事前計算済み) |
| データの鮮度 | 常に最新 | 常に最新 | REFRESHした瞬間だけ更新 |
| 元データが変わったら | 即反映 | 即反映 | REFRESHするまで反映されない |
| INSERT/UPDATE | 可能 | 基本不可 | 不可 |
| 主な用途 | データ格納 | JOINの再利用・簡略化 | 重い集計の高速化 |
flowchart TD
T["🗄️ Table\n実データの原本\nINSERT/UPDATE/DELETE可"]
V["👁️ View\nSELECT文の定義だけ保存\n実行のたびにTableを読む"]
MV["📦 Materialized View\nSELECT結果をコピー保存\nREFRESHするまで更新されない"]
T -->|"参照(SELECTのたびにクエリ実行)"| V
T -->|"REFRESH時にスナップショット取得"| MV
style T fill:#d4edda,stroke:#28a745
style V fill:#d1ecf1,stroke:#17a2b8
style MV fill:#fff3cd,stroke:#ffc107
よく使うSQL一覧(チートシート)
-- テーブル一覧
SELECT table_name FROM information_schema.tables
WHERE table_schema = 'public' AND table_type = 'BASE TABLE';
-- カラム一覧
SELECT column_name, data_type, is_nullable
FROM information_schema.columns WHERE table_name = 'users';
-- 全件取得
SELECT * FROM users;
-- 条件絞り込み
SELECT * FROM users WHERE id = 1;
-- 並び替え・件数制限
SELECT * FROM orders ORDER BY price DESC LIMIT 10;
-- 集計
SELECT user_id, COUNT(*), SUM(price) FROM orders GROUP BY user_id;
-- INNER JOIN
SELECT u.name, o.item_name FROM users u INNER JOIN orders o ON u.id = o.user_id;
-- LEFT JOIN
SELECT u.name, o.item_name FROM users u LEFT JOIN orders o ON u.id = o.user_id;
-- INSERT
INSERT INTO users (name, email) VALUES ('田中', 'taro@example.com') RETURNING id;
-- UPDATE
UPDATE users SET email = 'new@example.com' WHERE id = 1;
-- DELETE
DELETE FROM users WHERE id = 1;