DB/SQL

JOIN で複数テーブルを結ぶ

3
  • SQL

JOIN で複数テーブルを結ぶ

1 つの表だけだと「注文した人の名前」などがすぐ取れません。ここでは products · customers · orders を用意して JOIN でつなぎます。PostgreSQL 想定(MySQL もほぼ同じ)。下の準備で表をまとめて作ります。


目次

  1. なぜ表を分けるか
  2. 準備(customers / orders)
  3. INNER JOIN
  4. LEFT JOIN
  5. 複数 JOIN
  6. よくあるつまずき

なぜ表を分けるか

注文のたびに商品名・価格を全部書き写すと、値上げや改名のたびに過去行が食い違います。普通は次のように分けます。

  • customers … 誰か
  • products … 何を売っているか
  • orders … 誰が・何を・いつ・何個

orders には「実体のコピー」ではなく、customer_idproduct_id を持たせます。


準備(customers / orders)

cd $DB_SQL/PostgreSQL_setupdocker compose up -ddocker compose exec db psql -U practice -d practice_db
CREATE 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 JOININNER は省略できることが多いです。

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 の基本は十分です。

シェア