awesome-hacks
Docs

PostgreSQL入門 — 基本SQL完全ガイド

テーブル・カラム一覧の確認から JOIN・更新系SQL・ビューの違いまで、PostgreSQL初心者が現場で使える知識を一通り学ぶ

最終更新:2026/06/08

PostgreSQLとは

PostgreSQL(ポストグレスQL)は、世界で最も広く使われているオープンソースのリレーショナルデータベース(RDBMS)の一つ。
MySQLと並んでWebアプリのバックエンドで頻繁に採用され、NestJSやRailsとの組み合わせも一般的。

リレーショナルデータベースとは
データをテーブル(行と列の表)で管理し、テーブル同士をリレーション(関係)で結びつける仕組み。
Excelのシートをイメージするとわかりやすい。


基本用語の整理

用語意味Excelで例えると
データベースデータの格納場所(最上位の入れ物)Excelファイル
テーブル実際にデータが入る表シート
カラム(列)テーブルのデータ項目の定義列ヘッダー
レコード(行)テーブルの1件分のデータ1行
主キー(PK)レコードを一意に特定するカラム行番号(重複なし)
外部キー(FK)別テーブルの主キーを参照するカラム別シートへの参照

テーブル・カラム一覧の確認

MySQLでは SHOW TABLESSHOW COLUMNS を使うが、PostgreSQLでは独自の情報スキーマpsql のバックスラッシュコマンドを使う。

psqlで一覧を確認する(コマンドライン)

# データベース一覧
\l

# 接続先を切り替え
\c mydb

# テーブル一覧
\dt

# 特定テーブルのカラム情報
\d users

SQL でテーブル一覧を確認する

-- 現在のデータベース内のテーブル一覧
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_namedata_typeis_nullablecolumn_default
idintegerNOnextval(...)
namecharacter varyingNONULL
emailcharacter varyingNONULL
created_attimestampYESnow()

サンプルテーブルの定義

この記事では以下のテーブルを使って説明する。

-- ユーザーテーブル
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_idusers.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_idorder_counttotal_priceavg_price
13150005000
2180008000

主な集計関数:

関数意味
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_idnameitem_nameprice
1田中商品A3000
1田中商品B5000
2山田商品C8000
3佐藤NULLNULL

注文がない佐藤も行として含まれ、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を毎回書かなくてよくなる。
たとえば ordersusers を結合した 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;