「とりあえず結合」は卒業しよう。PostgreSQLのネステッドループ結合を味方につける方法
現場でSQLを書いていると、つい「JOIN」というコマンドに全幅の信頼を置いてしまいがちだよね。でも、PostgreSQLが裏でどうやってデータをくっつけているのか、その「戦略」を意識し始めると、パフォーマンスチューニングの景色が一気に変わってくる。
今日は、結合の基本中の基本にして、実は奥が深い「ネステッドループ結合(Nested Loop Join)」について深掘りしてみよう。
—
ネステッドループ結合とは何か?
一言で言えば、「二重ループ」だ。
コードで書くなら、こんなイメージ。
擬似コード
for row_outer in outer_table:
for row_inner in inner_table:
if match(row_outer, row_inner):
yield row_outer, row_inner
外側のテーブル(Outer)の行を1つ取り出し、内側のテーブル(Inner)を最初から最後まで探す。これを外側の全行分繰り返す。これがネステッドループの正体だよ。
「え、それって計算量がすごいことにならない?」と思った君は鋭い。その通り。データ量が多いと、この結合は致命的に遅くなる。でも、「ある条件」が揃うと、PostgreSQLにおいて最強の高速化エンジンに化けるんだ。
—
いつ「ネステッドループ」が光り輝くのか?
PostgreSQLのプランナがネステッドループを選択するのは、主に以下の2つのケースだ。
1. 外側のテーブルが十分に小さい場合
ループの回数が少なければ、オーバーヘッドはほとんどない。
2. 内側のテーブルの結合列に、強力なインデックスがある場合
ここが一番のポイントだ。もし内側のテーブルにインデックスがあれば、いちいち全件走査(シーケンシャルスキャン)しなくて済む。「インデックスを引いて、一瞬で該当行を見つける」という動きになるから、ループのコストが劇的に下がるんだ。
実践的な例:ユーザーと注文履歴
例えば、特定のユーザーの最新注文を1件だけ取得したいケースを考えてみよう。
SELECT u.name, o.order_id
FROM users u
INNER JOIN orders o ON u.id = o.user_id
WHERE u.id = 12345;
このとき、`orders`テーブルの`user_id`列にインデックスが貼られていれば、PostgreSQLは迷わずネステッドループを選ぶはずだ。
`users`テーブルからID:12345の行を1件見つけ(外側)、そのIDをキーにして`orders`テーブルのインデックスを直接叩く(内側)。これなら、数百万件の注文データがあっても、一瞬で結果が返ってくる。
—
注意すべき「罠」
逆に、ネステッドループが「地雷」になるパターンも覚えておいてほしい。
- インデックスがない場合:
内側のテーブルが数万件以上あるのに結合列にインデックスがないと、データベースは毎回フルスキャンを繰り返す。これは「結合地獄」の始まりだ。
- 外側のテーブルが大きすぎる場合:
いくら内側にインデックスがあっても、外側の行数が多すぎれば、インデックス検索の回数分だけコストが積み重なる。この場合、ハッシュ結合(Hash Join)の方が圧倒的に速い。
—
チューニングの現場で僕がやること
もしクエリが遅いと感じたら、まず`EXPLAIN ANALYZE`を叩くよね。そこで「Nested Loop」が出てきて、かつ実行時間が長いようなら、以下のチェックリストを回してみて。
- 「インデックスは効いているか?」:`Index Scan`や`Index Only Scan`になっているか確認する。もし`Seq Scan`になっていたら、即座にインデックスを追加する検討をする。
- 「結合の順序は最適か?」:実は結合順序を入れ替えるだけで、ネステッドループの効率が跳ね上がることがある。`FROM`句の書き方や、統計情報の更新(`ANALYZE`)が古いことが原因かもしれない。
—
まとめ
ネステッドループは、「小さな外側」と「インデックス付きの内側」という組み合わせにおいて、他の結合手法を寄せ付けない速さを誇る。
「ネステッドループ=遅い」というイメージを持っている人がたまにいるけれど、それは誤解だ。仕組みを理解して、適切なインデックスを配置してあげれば、これほど頼もしい相棒はいない。
まずは自分の書いたクエリが、どの結合手法を選んでいるのか。`EXPLAIN`の出力を見る癖をつけることから始めてみてほしい。それが、データベースエンジニアとしての第一歩だからね。
さて、次は「ハッシュ結合」の話でもしようか。また次の機会に!
コメント