やあ。今日もデータベースと格闘してるかい?
エンジニアが成長する過程で、必ず一度は深く向き合うことになるのが「実行計画(EXPLAIN)」だよね。その中でも、特に現場で一番よく見かける、そして意外と奥が深い「ネステッドループ結合(Nested Loop Join)」について、今日は少し踏み込んで話そうと思う。
教科書には「外側をループして、内側を検索する」と書いてあるけど、実務ではそれ以上のニュアンスが必要なんだ。
—
ネステッドループ結合の「本質」を理解しよう
ネステッドループの動きは、二重のfor文をイメージすれば完璧だ。
1. 外側(Outer)テーブルから1行取り出す。
2. その1行をキーにして、内側(Inner)テーブルを検索する。
3. これを外側テーブルの行数分だけ繰り返す。
非常にシンプルだよね。でも、このシンプルさが諸刃の剣なんだ。
どんな時に「最強」になるのか?
ネステッドループが輝くのは、「外側の抽出結果が少なく、かつ内側の結合キーにインデックスが貼られている時」だ。
例えば、ユーザーIDが固定された注文履歴を引っ張ってくるようなクエリ。
SELECT FROM users u
JOIN orders o ON u.id = o.user_id
WHERE u.id = 12345;
この場合、`users`テーブルから1行だけ取り出し、`orders`テーブルの`user_id`インデックスを引く。これはもう、爆速だよ。ディスクI/Oを最小限に抑えられるからね。
—
現場でハマる「ネステッドループの罠」
逆に、ネステッドループで「死ぬ」ケースを覚えておくと、トラブルシューティングのスピードが格段に上がる。
1. インデックスがない(または効かない)
内側テーブルの結合キーにインデックスがないと、PostgreSQLは毎回テーブル全体をフルスキャン(Seq Scan)することになる。外側が100万行あったら?……地獄だよね。クエリが全く終わらない時は、まずここを疑うべきだ。
2. 外側のデータ量が多すぎる
仮にインデックスがあっても、外側テーブルのレコードが数百万件あれば、内側へのインデックス検索も数百万回発生する。こうなると、メモリ効率や計算コストの面で、ハッシュ結合(Hash Join)やマージ結合(Merge Join)に軍配が上がる。
—
実践:実行計画を読んでみる
`EXPLAIN`の結果を見たときに、こんな光景に出くわすことがあるはずだ。
Nested Loop (cost=0.42..12.50 rows=1 width=128)
-> Index Scan using users_pkey on users (cost=0.42..4.44 rows=1 width=64)
Index Cond: (id = 12345)
-> Index Scan using orders_user_id_idx on orders (cost=0.00..8.05 rows=1 width=64)
Index Cond: (user_id = 12345)
この「上から下へ」流れるような構造が見えたら、ネステッドループが綺麗に決まっている証拠。もしここに `Seq Scan` が混じっていたら、インデックスの貼り忘れか、統計情報が古くなっている可能性が高い。
—
後輩へのアドバイス:どう使い分けるか
僕がチューニングする時は、こんな風に考えているよ。
- 「とりあえずの基本」: ネステッドループは、PostgreSQLのオプティマイザが好む「小規模な結合」の標準だ。まずはこれが選ばれるようなインデックス設計を心がけること。
- 「違和感を大切に」: 実行計画で「Nested Loop」が出ているのにレスポンスが遅いなら、それは「ループ回数が多すぎる」か「内側がインデックスを使えていない」のどちらかだ。迷わず `EXPLAIN (ANALYZE, BUFFERS)` を叩いて、実際のループ回数とコストを確認してほしい。
- 「統計情報を信じすぎない」: たまに、データ分布の偏りでオプティマイザが「ネステッドループが速い」と勘違いして、とんでもないフルスキャンを選択することがある。そんな時は `ANALYZE` で統計を更新するか、最悪の場合は結合の順序をヒント句的なアプローチで制御することを検討する。
—
最後に
ネステッドループは、データベースの挙動を知るための「最初の入り口」であり、同時に「最後の砦」でもある。
「なぜこのアルゴリズムが選ばれたのか?」を考える癖をつければ、君が書くSQLは一段上のレベルに到達するはずだ。難しく考えすぎず、まずは手元のクエリの実行計画を `EXPLAIN` してみることから始めてみて。
また面白いトピックがあったら話そう。それじゃ、良い開発ライフを!
コメント