サブクエリとEXISTS
三人の顧客と四件の注文を用意し、最大額の比較、IN、EXISTSを順に使います。注文のない顧客を探す際のNOT INとNULLの問題を再現し、相関条件を書き忘れた場合に対象が変わる理由も確かめます。
説明
部分問合せはSQLの中に書くSELECTです。一つの値、値の集合、行の存在を条件に使えます。外側の顧客IDを内側から参照する相関部分問合せでは、顧客ごとに対応する注文があるかを表現できます。
三人の顧客と四件の注文を用意し、最大額の比較、IN、EXISTSを順に使います。注文のない顧客を探す際のNOT INとNULLの問題を再現し、相関条件を書き忘れた場合に対象が変わる理由も確かめます。
1顧客と注文を用意する
顧客1に二件、顧客2に一件、顧客3に零件の注文がある状態を作ります。
CREATE TABLE customers顧客の表(id INTEGER PRIMARY KEY,name TEXT) STRICT;
INSERT INTO customers VALUES(1,'あおい'),(2,'はる'),(3,'みお');
CREATE TABLE orders注文の表(id INTEGER PRIMARY KEY,customer_id INTEGER REFERENCES customers(id),status TEXT,amount INTEGER) STRICT;
INSERT INTO orders VALUES(101,1,'paid',100),(102,1,'pending',200),(103,2,'cancelled',50),(104,NULL,'paid',80);
SELECT * FROM customers顧客一覧 ORDER BY id;
SELECT * FROM orders注文一覧 ORDER BY id;注文104は顧客IDがNULLで、会員登録のない注文です。顧客の表には存在しない特別な番号を使わず、参照がない状態として保存しています。このNULLがINとNOT INの結果に関係します。
2全注文の平均より高い注文
内側のSELECTで平均額を求めて条件に使います。
SELECT id,amount FROM orders WHERE amount>(SELECT AVG(amount) FROM orders)条件に使う単一の値 ORDER BY id;平均は(100+200+50+80)/4=107.5円なので、注文102の200円だけです。括弧内は一行一列の平均値を返し、外側の各注文額と比較されます。
3注文に現れる顧客IDを候補にする
注文のcustomer_idをINの候補にします。
SELECT id,name FROM customers WHERE id IN (SELECT customer_id FROM orders)注文に含まれるIDへ一致 ORDER BY id;あおいとはるが選ばれます。候補には1が二回ありますが、外側の顧客行を複製することはありません。INは候補のどれかに一致するかを条件にします。
4EXISTSで対応する注文の存在を調べる
内側で外側の顧客IDを参照します。
SELECT c.id,c.name FROM customers AS c WHERE EXISTS (SELECT 1一行でも存在するか FROM orders AS o WHERE o.customer_id=c.id外側の顧客に対応する条件) ORDER BY c.id;同じ二人が選ばれます。EXISTSは部分問合せが一行以上返すかで真偽を決めます。SELECT 1の1を合計するのではありません。あおいに注文が二件あっても、顧客は一行のままです。
5150円以上の注文を持つ顧客
存在判定の内側へ金額条件を追加します。
SELECT c.id,c.name FROM customers AS c WHERE EXISTS (SELECT 1 FROM orders AS o WHERE o.customer_id=c.id顧客ごとの注文 AND o.amount>=150存在してほしい注文の金額条件) ORDER BY c.id;あおいだけです。顧客IDが一致し、かつ150円以上という両条件を満たす注文102が存在します。どの注文が条件に合うかと、顧客を一行残すかを分けて表現できます。
6相関条件を外すと全顧客が残る
顧客IDの対応条件だけを取り除きます。
SELECT c.id,c.name FROM customers AS c WHERE EXISTS (SELECT 1 FROM orders AS o WHERE o.amount>=150外側の顧客IDを参照しない条件) ORDER BY c.id;三人とも選ばれます。内側が問うのは「表全体に150円以上の注文があるか」だけになり、外側の顧客によらず真です。顧客3自身に注文があるという意味にはなりません。
7NOT INにNULLが含まれる場合
注文の顧客ID候補に含まれない顧客を探します。
SELECT id,name FROM customers WHERE id NOT IN (SELECT customer_id FROM orders)NULLも含む候補の否定 ORDER BY id;結果は0行です。顧客1と2は候補にあるため偽、顧客3は候補内のNULLとの比較を確定できずNULLになります。WHEREはNULLの条件を残さないので、注文のないみおも出ません。
8候補からNULLを除いて比較する
内側へIS NOT NULLを追加します。
SELECT id,name FROM customers WHERE id NOT IN (SELECT customer_id FROM orders WHERE customer_id IS NOT NULL候補からNULLを除く) ORDER BY id;みおだけになります。この例の外側idは主キーでNULLではありません。内側のNULLを除けば、残る候補1・1・2のどれにも一致しない3を確定できます。
9対応する注文が存在しない顧客
NOT EXISTSで顧客に対応する注文の不存在を判定します。
SELECT c.id,c.name FROM customers AS c WHERE NOT EXISTS (SELECT 1対応する行が一件もないか FROM orders AS o WHERE o.customer_id=c.id顧客ごとの対応条件) ORDER BY c.id;みおだけです。顧客IDの一致する注文行が一行もないため、NOT EXISTSが真になります。注文104のNULLは誰とも一致せず、他の顧客の不存在判定を妨げません。
10顧客ごとの最大注文額を列として返す
相関部分問合せで最大額を一列へ表示します。
SELECT c.id,c.name,(SELECT MAX(o.amount)顧客ごとの最大額 FROM orders AS o WHERE o.customer_id=c.id)外側の顧客に限定 AS max_amount FROM customers AS c ORDER BY c.id;あおい200円、はる50円、みおNULLです。MAXの集約は該当注文がなくてもNULLの一行を返します。外側の三人はWHEREで絞っていないので全員残ります。
11複数行を返す部分問合せを単一値に使うとき
SQLiteの単一値用の部分問合せで最古の注文を明示します。
SELECT c.id,(SELECT o.amount FROM orders AS o WHERE o.customer_id=c.id ORDER BY o.id LIMIT 1どの一行を選ぶか明示) AS first_amount FROM customers AS c ORDER BY c.id;あおい100円、はる50円、みおNULLです。SQLiteは単一値の部分問合せで先頭行を使いますが、ORDER BYなしの先頭を期待するべきではありません。ここでは注文IDの小さい順とLIMIT 1を明示し、最大額とは別の値を求めています。
まとめ
部分問合せを単一値・候補集合・存在判定のどれとして使うかを明確にします。
EXISTSの相関条件を外すと、顧客ごとの存在判定から表全体の存在判定へ変わります。
NOT INのNULLを考慮し、対応行がないことはNOT EXISTSでも表現できます。
操作履歴と解説を振り返れます。
SQLite 3.54.0
CREATE TABLE customers(id INTEGER PRIMARY KEY,name TEXT) STRICT;
INSERT INTO customers VALUES(1,'あおい'),(2,'はる'),(3,'みお');
CREATE TABLE orders(id INTEGER PRIMARY KEY,customer_id INTEGER REFERENCES customers(id),status TEXT,amount INTEGER) STRICT;
INSERT INTO orders VALUES(101,1,'paid',100),(102,1,'pending',200),(103,2,'cancelled',50),(104,NULL,'paid',80);
SELECT * FROM customers ORDER BY id;
SELECT * FROM orders ORDER BY id;╭────┬────────╮ │ id │ name │ ╞════╪════════╡ │ 1 │ あおい │ │ 2 │ はる │ │ 3 │ みお │ ╰────┴────────╯ ╭─────┬─────────────┬───────────┬────────╮ │ id │ customer_id │ status │ amount │ ╞═════╪═════════════╪═══════════╪════════╡ │ 101 │ 1 │ paid │ 100 │ │ 102 │ 1 │ pending │ 200 │ │ 103 │ 2 │ cancelled │ 50 │ │ 104 │ NULL │ paid │ 80 │ ╰─────┴─────────────┴───────────┴────────╯
SELECT id,amount FROM orders WHERE amount>(SELECT AVG(amount) FROM orders) ORDER BY id;╭─────┬────────╮ │ id │ amount │ ╞═════╪════════╡ │ 102 │ 200 │ ╰─────┴────────╯
SELECT id,name FROM customers WHERE id IN (SELECT customer_id FROM orders) ORDER BY id;╭────┬────────╮ │ id │ name │ ╞════╪════════╡ │ 1 │ あおい │ │ 2 │ はる │ ╰────┴────────╯
SELECT c.id,c.name FROM customers AS c WHERE EXISTS (SELECT 1 FROM orders AS o WHERE o.customer_id=c.id) ORDER BY c.id;╭────┬────────╮ │ id │ name │ ╞════╪════════╡ │ 1 │ あおい │ │ 2 │ はる │ ╰────┴────────╯
SELECT c.id,c.name FROM customers AS c WHERE EXISTS (SELECT 1 FROM orders AS o WHERE o.customer_id=c.id AND o.amount>=150) ORDER BY c.id;╭────┬────────╮ │ id │ name │ ╞════╪════════╡ │ 1 │ あおい │ ╰────┴────────╯
SELECT c.id,c.name FROM customers AS c WHERE EXISTS (SELECT 1 FROM orders AS o WHERE o.amount>=150) ORDER BY c.id;╭────┬────────╮ │ id │ name │ ╞════╪════════╡ │ 1 │ あおい │ │ 2 │ はる │ │ 3 │ みお │ ╰────┴────────╯
SELECT id,name FROM customers WHERE id NOT IN (SELECT customer_id FROM orders) ORDER BY id;SELECT id,name FROM customers WHERE id NOT IN (SELECT customer_id FROM orders WHERE customer_id IS NOT NULL) ORDER BY id;╭────┬──────╮ │ id │ name │ ╞════╪══════╡ │ 3 │ みお │ ╰────┴──────╯
SELECT c.id,c.name FROM customers AS c WHERE NOT EXISTS (SELECT 1 FROM orders AS o WHERE o.customer_id=c.id) ORDER BY c.id;╭────┬──────╮ │ id │ name │ ╞════╪══════╡ │ 3 │ みお │ ╰────┴──────╯
SELECT c.id,c.name,(SELECT MAX(o.amount) FROM orders AS o WHERE o.customer_id=c.id) AS max_amount FROM customers AS c ORDER BY c.id;╭────┬────────┬────────────╮ │ id │ name │ max_amount │ ╞════╪════════╪════════════╡ │ 1 │ あおい │ 200 │ │ 2 │ はる │ 50 │ │ 3 │ みお │ NULL │ ╰────┴────────┴────────────╯
SELECT c.id,(SELECT o.amount FROM orders AS o WHERE o.customer_id=c.id ORDER BY o.id LIMIT 1) AS first_amount FROM customers AS c ORDER BY c.id;╭────┬──────────────╮ │ id │ first_amount │ ╞════╪══════════════╡ │ 1 │ 100 │ │ 2 │ 50 │ │ 3 │ NULL │ ╰────┴──────────────╯
演習完了