目次
ネステッドループ結合とは(動かして確認)
DBがテーブルを結合するときの最も基本的な方法がNested Loop Joinです。
Nested Loop Joinは、 外側テーブルを1行読むたびに内側テーブルを走査します。
ここでは、1ステップずつ動かし、 比較回数と走査回数がどう積み上がるか確認できます。
以下のSQLを実行する場合です。
| select a.氏名, b.部署名 from 社員 a -- 外側テーブル (5行) join 部署 b -- 内側テーブル (3行) on a.部署ID = b.id; |
「1ステップ進む」をクリックすると動きます。
以下どちらかを選択できます。
・単純ループ(単純ネステッドループ)
・インデックス(インデックスネステッドループ)
| id | 氏名 | 部署ID |
|---|
「1ステップ進む」を押すと、処理が1つずつ進みます。
| id | 部署名 |
|---|
単純ネステッドループ
| 氏名 | 部署名 |
|---|
説明の要点
コストは N × M で伸びる
外側N行×内側M行。外側が1,000行・内側が10,000行なら比較は1,000万回。行数が増えると急激に破綻するのが単純ループの弱点です。
索引は「辞書を引く」動き
索引があれば、内側は先頭から読まずに済みます。辞書は厚くなっても探す手間がほとんど増えないのと同じで、内側が3行でも100万行でも1行につき1回引くだけ。増えるのは1回あたりの深さで、たかだか数段です。
外側は小さいほうを置く
外側の行数が、そのまま内側の走査回数になります。オプティマイザが結合順を入れ替えるのは、この回数を減らすためです。
一致しない行は消える
伊藤(部署ID=4)はどの部署にも一致せず、内部結合の結果から落ちます。LEFT JOIN ならNULL埋めで残ります。
外側テーブルと内側テーブルは、どうやって決まるのか
SQL の FROM ... JOIN ... の書き順では決まりません。オプティマイザが両方の割り当てを試算し、 安いほうを選びます。判断のもとになるのは、この一本の式だけです。
| 外側の行数×内側を1回調べる手間 |
効き大 内側の結合キーに索引があるか
索引があれば内側は引くだけ、無ければ毎回全部読む。行数が増えるほど差が開きます。そのため実務では索引を持っているほうが内側に回り、残りが外側という決まり方が大半です。そもそもプランナがNested Loopを選ぶのは、ほぼこの形が成立するときだけです。
効き中 絞り込んだ後の行数(テーブルの大きさではない)
外側の行数が、そのまま内側を調べる回数になります。だからWHEREで絞った後に小さくなるほうが外側。1億行のテーブルでもWHEREid=42が付いていれば見積り1行、堂々の駆動表です。効くのは統計情報からの推定行数であって、実テーブルの行数ではありません。
前提 コスト以前に、役割が固定される場合
LEFT JOINは残すほう(左側)が外側に固定。相関サブクエリやLATERALは、外側の値に依存する側が必ず内側になります。EXISTS/NOT EXISTSも探索の向きが決まっています。ただしLEFT JOINも、WHEREに内側列のNULL除去条件が付くと内部結合に書き換えられ、再び入れ替え可能になります。
※詳細の説明は下の「外側・内側の決まり方には2段階の判断がある」に記載。
書き順が効いてしまう例外
| PostgreSQL | 明示的JOINの数がjoin_collapse_limit(既定8)を超えると、探索を諦めて書いた順のまま組みます。テーブル数の多いクエリで順序が効き出すのはこれ。 |
| MySQL | STRAIGHT_JOINで駆動表を固定できます |
| Oracle | LEADING / ORDEREDヒント |
| SQL Server | OPTION (FORCE ORDER) |
実際にどちらが外側になったか確認する
| EXPLAIN (ANALYZE, BUFFERS) SELECT e.氏名, d.部署名 FROM 社員 e JOIN 部署 d ON e.部署ID = d.id; |
| PostgreSQL | NestedLoopの1番目の子が外側、2番目が内側。内側ノードのloops=が外側の行数と一致します(=走査回数そのもの) |
| MySQL | EXPLAIN の 1行目が駆動表(=外側) |
| Oracle | NESTED LOOPS の第1子が駆動表 |
(前提)外側・内側の決まり方には2段階の判断がある
まず結合の意味を壊さないために向きが強制されるケースかどうかを先に見て、
そうでなければ次にどちらがコスト的に有利かで決める、という順序です。
LEFTJOIN・相関サブクエリ・LATERAL・EXISTS/NOTEXISTSは前者にあたり、
単純なINNERJOINや、条件によって実質INNERJOINに簡約されたLEFTJOINは後者にあたります。
LEFT JOIN
LEFT JOINはマッチしなくても左側の行は必ず残すという意味を持っています。この保証を守るには、左側の行を基準にループを回し、右側(内側)にマッチがなければNULL埋めで出力する、という処理順序が必要です。右側を外側にしてしまうと、この左側は全部出すという意味論が壊れてしまうため、左側=外側に固定されます。
相関サブクエリ・LATERAL
相関サブクエリは、サブクエリの中で外側クエリの列を参照しています。つまり、サブクエリを評価するには先に外側の行の値が確定していなければなりません。データの依存関係が外側→内側という向きしか成立しないため、必然的に外側の値に依存する側が内側になります。LATERALも同じ理屈で、構文自体が左側の行を使って右側の式を評価するという依存関係を明示しているので、左側=外側、LATERAL側=内側に固定されます。
EXISTS / NOT EXISTS
これも中身は相関サブクエリと同じで、外側クエリの各行についてそのつど内側の存在チェックを行う、という探索の向きが構造上決まっています。NOT EXISTSも同様に、内部的には反結合(アンチジョイン)として扱われますが、ネステッドループで実行する場合の探索方向は固定されたままです。
LEFT JOINがINNER JOINに書き換わるケース
一方で、LEFT JOINにWHERE句で内側テーブルの列に対する条件(例えば WHERE p.status = 'paid' のように、マッチしなかった行=NULLになる行を弾いてしまう条件)が付くと、話が変わります。この条件によってマッチしなくても左側を残すというLEFT JOINの効果自体が実質的に無効化されてしまうため、結果的にはINNER JOINと同じ意味になります。
この場合オプティマイザはこれは実質INNER JOINだと判断してLEFT JOINをINNER JOINに書き換え(結合の簡約化)、その時点で外側・内側の役割は再びコストベースで自由に選べる状態に戻ります。
関連の記事
