サブクエリと EXISTS で条件を組み立てる
JOIN だけでも多くのことはできますが、「一覧のうち、注文がある人だけ」「平均より高い商品」などはサブクエリの方が読みやすいことがあります。products · customers · orders を用意して進めます。所要は 25〜35 分。
目次
準備
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 COUNT(*) FROM orders;SELECT AVG(price) FROM products;スカラーサブクエリ
1 行 1 列を返すサブクエリです。平均より高い商品:
SELECT name, priceFROM productsWHERE price > (SELECT AVG(price) FROM products)ORDER BY price;括弧の中が先に評価され、その結果と比較されます。複数行を返す書き方をここに置くとエラーになります。
IN サブクエリ
注文されたことがある商品:
SELECT name, priceFROM productsWHERE id IN (SELECT product_id FROM orders);注文したことがある顧客:
SELECT name, emailFROM customersWHERE id IN (SELECT customer_id FROM orders);否定は NOT IN ですが、サブクエリ側に NULL が混ざると結果が空になりやすいので、実務では NOT EXISTS の方が安全なことが多いです。よくあるのは NOT IN で全部空になり、原因が NULL だと気づくまで遠回りすることです。
EXISTS
「関連行が1件でもあるか」だけ見ます。中の SELECT リストは何でもよく、慣習で SELECT 1 と書くことが多いです。
SELECT c.nameFROM customers cWHERE EXISTS ( SELECT 1 FROM orders o WHERE o.customer_id = c.id);注文が無い顧客(鈴木さん):
SELECT c.nameFROM customers cWHERE NOT EXISTS ( SELECT 1 FROM orders o WHERE o.customer_id = c.id);外側の行ごとに内側を相関させるので、相関サブクエリと呼ばれます。LEFT JOIN ... WHERE o.id IS NULL と同じ結果を、EXISTS でも書けます。
JOIN との使い分け
| 書き方 | 向いていること |
|---|---|
| JOIN | 両表の列を結果に出したい |
| IN / EXISTS | 「いる/いない」のフィルタが主目的 |
| スカラーサブクエリ | 平均・最大など単一値との比較 |
同じ結果でも、計画(EXPLAIN)は変わることがあります。まずは読みやすさで選び、遅くなったら EXPLAIN で計画を見る、で十分です。
JOIN で同じ「注文あり顧客」:
SELECT DISTINCT c.nameFROM customers cJOIN orders o ON o.customer_id = c.id;よくあるつまずき
| 症状 | 対処 |
|---|---|
more than one row returned by a subquery |
スカラー用の場所に複数行。IN / EXISTS に変える |
NOT IN が常に空 |
サブクエリに NULL。NOT EXISTS へ |
| 結果が遅い | 相関が重い可能性。JOIN 化や索引を検討 |
EXISTS と JOIN で同じ絞り込みが書ければ十分です。遅さが気になったら EXPLAIN で計画を見てみてください。