ぱと隊長日誌

ブログ運用もエンジニアとしての生き方も模索中

SQLのWHERE句で用いられる相関サブクエリを理解する

はじめに

相関サブクエリもしくはEXISTS述語の使い方として、以下のようなSQLがよく取り上げられます。

SELECT *
  FROM item i
 WHERE EXISTS
  (SELECT *
     FROM stock s
    WHERE i.id = s.id);

これに対して以下のデータが与えられたとします。

◆itemテーブル

id name
1 みかん
2 バナナ
3 リンゴ
4 ぶどう

◆stockテーブル

id quantity
1 10
3 30

この時、EXISTS述語内のサブクエリ"SELECT * FROM stock s WHERE i.id = s.id"だけを見て、以下のように考えてしまうことは無いでしょうか?
※補足:このサブクエリ単体では"i"を定義していないため、エラーとなります。実行するにはFROM句に"item i"の追加が必要です。以下同様です。

EXISTS述語内のサブクエリが2行返す
  ↓
EXISTS述語の評価がTRUEとなる
  ↓
WHERE句の評価がTRUEとなる
  ↓
WHERE句がTRUEのためitemテーブルの全行を返す

ですが、最終的な結果としては2行しか返ってきません。
どこで勘違いが起きてしまったのでしょうか?本エントリではこの疑問について説明します。

理解のポイントはWHERE句の処理にある

先日、WHERE句について解説するエントリをアップしました。
SQLのWHERE句の処理を理解する - ぱと隊長日誌
このエントリでSQLのWHERE句を「集合から条件に合致する行を選択する」処理であり、WHERE句の評価は1行ずつ行われる、と説明しました。
これを理解できればWHERE句の相関サブクエリを理解できます。冒頭の例について、処理の流れに沿いながら確認していきましょう。

(1)
FROM句でitemテーブルが指定されているため、対象となる集合は以下となります。

i.id i.name
1 みかん
2 バナナ
3 リンゴ
4 ぶどう

(2)
WHERE句は1行ずつ処理されるため、まずは先頭の行を評価します。

i.id i.name
1 みかん

(3)
ここでEXISTS述語内のサブクエリを実行します。

SELECT *
  FROM stock s
 WHERE i.id = s.id;

WHERE句が評価しているのは"i.id = 1"ですので、このクエリは以下のように動きます。

SELECT *
  FROM stock s
 WHERE 1 = s.id;

このクエリの結果を示します。

s.id s.quantity
1 10

(4)
サブクエリが1行返したため、EXISTS述語はTRUEとなります。
結果、WHERE句はTRUEとなり、WHERE句の評価していた行が選択されます。

i.id i.name
1 みかん

(5)
以降、対象となる集合の全ての行を処理するまで(3)から繰り返します。
結果として以下の行が選択されます。

i.id i.name
1 みかん
3 リンゴ

WHERE句が常にTRUEとなると勘違いをしてしまった方は、相関サブクエリがitemテーブルとstockテーブルの各集合に対して実行されると考えたのではないでしょうか。
処理を正しく理解するためには、WHERE句はitemテーブルの各行に対して評価されるため、itemテーブルのWHERE句で評価されている行とstockテーブルの集合に対してサブクエリが実行されると理解することが必要です。

単なる(相関でない)サブクエリであればどうなるのか?

このようなSQLであればどうでしょうか?

SELECT *
  FROM item i
 WHERE i.id = (SELECT max(s.id)
                 FROM stock s);

先ほどの例と同様にitemの各行毎にサブクエリが実行されると考えても構いませんが、サブクエリだけが先に実行されると考えても同じ結果となります。なぜなら、外側のクエリで使われているitemテーブルの列をサブクエリで参照していないからです。WHERE句がitemテーブルのどの行を評価していたとしても、サブクエリの結果は変わりません。

サブクエリを先に実行すると以下のSQLとなります。

SELECT *
  FROM item i
 WHERE i.id = 3;

結果は以下の通りです。

i.id i.name
3 リンゴ

まとめ

SQLを理解するためには処理の流れを理解することが大切です。
今回の例でいえば、WHERE句の処理を理解せずに()で囲まれた箇所を先に評価すると理解してしまうと失敗します。

この誤った理解でも以下のSQLは正しく評価できます。

SELECT *
  FROM item i
 WHERE i.id = (SELECT max(s.id)
                 FROM stock s);

でも、別のSQLは正しく評価できません。

SELECT *
  FROM item i
 WHERE EXISTS (SELECT *
                 FROM stock s
                WHERE i.id = s.id);

このエントリがSQLを理解するための一歩となれば幸いです。

参考資料

プログラマのためのSQL 第4版

プログラマのためのSQL 第4版

プログラマのためのSQL 第4版

SQLが途中どのように処理されて最終結果に至るかを細かく解説しています。この途中処理こそがSQLを理解するためのカギとなります。
かなりの分量ですので通読は難しくとも、SQLを書いていて気になったことを調べるときに必要な箇所を参照してみてはいかがでしょうか。

達人に学ぶSQL徹底指南書

達人に学ぶ SQL徹底指南書 (CodeZine BOOKS)

達人に学ぶ SQL徹底指南書 (CodeZine BOOKS)

EXISTS述語を述語論理の観点から解説しています。また、相関サブクエリの応用例についても説明されています。