JOIN で複数テーブルを結ぶ
1 つの表だけだと「注文した人の名前」などがすぐ取れません。ここでは products · customers · orders を用意して JOIN でつなぎます。PostgreSQL 想定(MySQL もほぼ同じ)。下の準備で表をまとめて作ります。
目次
なぜ表を分けるか
注文のたびに商品名・価格を全部書き写すと、値上げや改名のたびに過去行が食い違います。普通は次のように分けます。
customers… 誰かproducts… 何を売っているかorders… 誰が・何を・いつ・何個
orders には「実体のコピー」ではなく、customer_id や product_id を持たせます。
準備(customers / orders)
cd $DB_SQL/PostgreSQL_setupdocker compose up -ddocker compose exec db psql -U practice -d practice_dbCREATE TABLE IF NOT EXISTS products ( id SERIAL PRIMARY KEY, name TEXT NOT NULL, category TEXT NOT NULL, price INT NOT NULL);CREATE TABLE IF NOT EXISTS customers ( id SERIAL PRIMARY KEY, name TEXT NOT NULL, email TEXT NOT NULL UNIQUE);CREATE TABLE IF NOT EXISTS orders ( id SERIAL PRIMARY KEY, customer_id INT NOT NULL REFERENCES customers(id), product_id INT NOT NULL REFERENCES products(id), qty INT NOT NULL CHECK (qty > 0), ordered_at DATE NOT NULL DEFAULT CURRENT_DATE);TRUNCATE orders, customers, products RESTART IDENTITY;INSERT INTO products (name, category, price) VALUES ('ノート A5', 'stationery', 300), ('ボールペン', 'stationery', 120), ('USB-C ケーブル', 'gadget', 980), ('ワイヤレスマウス', 'gadget', 2500);INSERT INTO customers (name, email) VALUES ('山田', 'yamada@example.com'), ('佐藤', 'sato@example.com'), ('鈴木', 'suzuki@example.com'); -- まだ注文なしINSERT INTO orders (customer_id, product_id, qty, ordered_at) VALUES (1, 1, 2, '2026-08-01'), (1, 3, 1, '2026-08-02'), (2, 2, 5, '2026-08-03'), (2, 4, 1, '2026-08-03');単表の確認:
SELECT * FROM customers;SELECT * FROM orders;INNER JOIN
両方にマッチする行だけ残します。注文がある人・商品だけ、というイメージです。
SELECT o.id AS order_id, c.name AS customer, p.name AS product, o.qty, p.price, o.qty * p.price AS line_totalFROM orders oINNER JOIN customers c ON o.customer_id = c.idINNER JOIN products p ON o.product_id = p.idORDER BY o.id;期待する結果(4 行):
| order_id | customer | product | qty | price | line_total |
|---|---|---|---|---|---|
| 1 | 山田 | ノート A5 | 2 | 300 | 600 |
| 2 | 山田 | USB-C ケーブル | 1 | 980 | 980 |
| 3 | 佐藤 | ボールペン | 5 | 120 | 600 |
| 4 | 佐藤 | ワイヤレスマウス | 1 | 2500 | 2500 |
o / c / p は別名です。長い表名を何度も書かなくて済みます。
INNER JOIN の INNER は省略できることが多いです。
SELECT c.name, p.name, o.qtyFROM orders oJOIN customers c ON o.customer_id = c.idJOIN products p ON o.product_id = p.id;鈴木さんは注文が無いので、この結果には出てきません。
LEFT JOIN
左側の表は残し、右側に無いときは NULL になります。「顧客一覧に、注文の有無を足したい」とき向けです。
SELECT c.name, o.id AS order_id, o.ordered_atFROM customers cLEFT JOIN orders o ON o.customer_id = c.idORDER BY c.id, o.id;鈴木さんの行は order_id が空(NULL)になります。ここが INNER との差です。よくあるのは INNER のまま顧客一覧を取ろうとして、注文の無い人が消えるパターンです。
注文が無い顧客だけ:
SELECT c.nameFROM customers cLEFT JOIN orders o ON o.customer_id = c.idWHERE o.id IS NULL;複数 JOIN
すでに上で customers と products の両方を結んでいます。実務でも「事実の表(orders)を軸に、マスタを足す」形が多いです。
日付で絞る例:
SELECT c.name, p.name, o.qty, o.ordered_atFROM orders oJOIN customers c ON o.customer_id = c.idJOIN products p ON o.product_id = p.idWHERE o.ordered_at >= DATE '2026-08-02'ORDER BY o.ordered_at;よくあるつまずき
| 症状 | 対処 |
|---|---|
| 行が増えすぎる | 結合条件が弱い(同じ id 同士を結んでいない等)。ON を見直す |
| 鈴木が出てこない | INNER を使っている。一覧に残したいなら LEFT |
column reference is ambiguous |
name など両表にある列。c.name のように修飾する |
| 外部キーエラーで INSERT できない | 先に customers / products を入れてから orders |
掃除:
DROP TABLE IF EXISTS orders, customers, products;INNER と LEFT の差が鈴木さんの行で見えれば、JOIN の基本は十分です。