変更要約: 初版
3.3結合と副問合せ
内部結合/外部結合/自己結合の使い分け、相関副問合せと非相関副問合せの性能・意味の違い、集合演算(UNION/INTERSECT/EXCEPT)、そしてNULLと3値論理が条件式や結合結果に与える影響を、実務クエリの正確性判断とともに学びます。
結合と副問合せは同じ結果を複数の書き方で表現できることが多く、どちらが正しく・どちらが速いかを判断できることがDBスペシャリストの実務力です。特にNULLの扱いは、条件式の評価が「真」「偽」に加えて「不明(UNKNOWN)」を持つ3値論理であることに起因して、初学者が誤りやすい領域です。この節では結合方式の選択、副問合せの種類、集合演算、NULLの罠を扱います。
3.3.1結合方式の使い分け
- 「注文がある顧客だけを見たい」なら内部結合(INNER JOIN)で十分だが、「注文の有無にかかわらず全顧客を見て、注文がなければNULLで示したい」場合は左外部結合(LEFT JOIN)が必要。結合方式を誤ると、実在するのに結果から漏れる行が出るという実務上のバグになりやすい。
- 外部結合の後に
WHERE句で結合先の列に条件を付けると、その条件が暗黙にNULLを除外し内部結合と同じ結果に戻ってしまうことがある(例:LEFT JOIN orders ON … WHERE orders.status = 'shipped'は、注文がない顧客の行を除外してしまう)。この場合は条件をON句側に書くか、WHEREでIS NULLも許容する必要がある。
3.3.2相関副問合せと非相関副問合せ
- 非相関副問合せ=外側のクエリの値を参照せず、独立に1回だけ実行できる副問合せ。結果が1つの値や固定リストになるため、
INや比較演算子と組み合わせやすい。 - 相関副問合せ=外側のクエリの各行の値を参照し、外側の行ごとに副問合せが再評価される(概念的には行数分繰り返し実行)。
EXISTS/NOT EXISTSと組み合わせて「関連する行があるか/ないか」を判定する典型パターンで、先述の商(÷)の二重NOT EXISTSもこの一種。 - 相関副問合せは概念上は行数分の繰り返し実行だが、多くのオプティマイザは相関副問合せを結合(セミジョイン/アンチセミジョイン)に書き換えて効率化するため、素朴な実装のまま性能が線形に悪化するとは限らない。ただし複雑な副問合せではオプティマイザが書き換えに失敗し、実際に行数分実行されて性能劣化する場合もあるため、実行計画の確認が欠かせない。
3.3.3集合演算とNULLの3値論理
- UNION=2つの問い合わせ結果を合わせ重複を除去(
UNION ALLは重複を残す)。INTERSECT=両方に共通する行だけを残す。EXCEPT=一方にはあり他方にはない行を残す。いずれも列数・データ型の対応(和両立)が必要。 - SQLの条件式は真(TRUE)・偽(FALSE)に加えて不明(UNKNOWN)を持つ3値論理で評価される。NULLとの比較(
= NULL等)は常にUNKNOWNになり、WHERE句はUNKNOWNの行を結果に含めない(TRUEの行だけを残す)。 - NULLの判定には
= NULLではなくIS NULL/IS NOT NULLを用いる必要がある。NOT IN副問合せの結果リストにNULLが1件でも混入すると、比較がUNKNOWNになり全体の結果が意図せず空になるという著名な罠がある(NOT EXISTSならこの問題を回避できる)。
「外部結合後にWHEREで結合先列へ条件を付けると内部結合相当に戻ってしまう」「NOT IN副問合せの結果にNULLが混入すると全体が空になる(NOT EXISTSで回避)」「NULLとの比較は常にUNKNOWNでありIS NULLが必須」が最頻出です。相関副問合せは多くの場合オプティマイザが結合に書き換える点も押さえましょう。
あるアプリ開発者が「注文をキャンセルした顧客を除いた全顧客一覧」を出すクエリをcustomer_id NOT IN (SELECT customer_id FROM cancellations)と書いたところ、顧客が1人も表示されないという不具合を報告してきたとします。原因調査の第一歩はcancellations.customer_id列にNULLが含まれていないかを確認することです。もしcancellations表に、システム的な理由でcustomer_idが未確定(NULL)のまま登録された行が1件でもあれば、NOT IN副問合せの結果リストにはNULLが混入し、3値論理により「customer_id NOT IN (…, NULL, …)」の比較が全ての行でUNKNOWNとなり、WHERE句がどの行も残さないという事象が発生します。この場合の修正方針は2つあります。
①副問合せ側にWHERE customer_id IS NOT NULLを追加してNULLを除外する、
②NOT INをNOT EXISTS(相関副問合せ)に書き換える——NOT EXISTS (SELECT 1 FROM cancellations c WHERE c.customer_id = customers.customer_id)は比較のたびに個別の等価判定を行うためNULL混入の影響を受けません。実務では再発防止の観点から
②のNOT EXISTS化を標準とし、
①はその場しのぎの対症療法と位置づけるのが妥当です。なぜなら、将来他の開発者が同様のNOT IN副問合せを書いた際に同じ罠を踏む可能性があるため、チームのコーディング規約として「除外条件の副問合せは原則NOT EXISTSを使う」と定めるほうが根本的な対策になるからです。
| 式 | 評価結果 |
|---|---|
| NULL = NULL | UNKNOWN(TRUEにはならない) |
| 列 = NULL | UNKNOWN |
| 列 IS NULL | TRUE または FALSE(NULLかどうかを正しく判定) |
| NOT IN リスト内にNULLが混入 | 比較全体がUNKNOWNとなり結果が空になりうる |
ひっかけ: 「NOT INとNOT EXISTSは常に同じ結果を返すので置き換え自由」は誤りです——副問合せの結果にNULLが含まれる場合、NOT INは意図せず結果が空になりうるがNOT EXISTSは影響を受けないという重大な差があります。また「相関副問合せは常に行数分実行され低速」も誤り=多くのオプティマイザが結合に書き換え効率化するため一概には言えません。
3.3.4この節のまとめ
- 外部結合後に結合先列へ
WHERE条件を付けると内部結合相当に戻る罠がある。条件はON句かNULL許容の書き方にする - NOT INの副問合せ結果にNULLが混入すると結果が空になる罠があり、NOT EXISTSで回避できる
- NULLとの比較は3値論理でUNKNOWNになるため、NULL判定は
=ではなくIS NULLを使う
進捗の記録にはログインが必要です。
理解度チェック
(軽い確認用)Q1. `customer_id NOT IN (SELECT customer_id FROM cancellations)`というクエリが、実際にはキャンセルしていない顧客も含め1件も結果を返さない。最も可能性が高い原因はどれか。
Q2. 注文の有無にかかわらず全顧客を一覧表示したいが、`LEFT JOIN orders ON customers.id = orders.customer_id WHERE orders.status = 'shipped'`と書いたところ、注文のない顧客が結果から消えてしまった。適切な修正はどれか。
Q3. 相関副問合せを用いたクエリの性能について、最も正確な説明はどれか。

