【実務・中級編】 ネステッドループ結合のチューニング – PostgreSQL

やあ。最近、PostgreSQLのクエリが「なぜか遅い」って悩んでない?
パフォーマンスチューニングの世界に足を踏み入れると、まず最初にぶち当たる壁が「結合(JOIN)」の戦略だよね。

特に、PostgreSQLが選ぶ「ネステッドループ結合(Nested Loop Join)」は、一見シンプルだけど、扱い方を間違えるとシステム全体の足を引っ張る大きなリスクを孕んでいるんだ。今日は、このネステッドループの「正体」と、僕が現場でよくやる最適化の勘所を共有するよ。

—

ネステッドループ結合って、結局なんなの?

簡単に言えば、「二重ループ」だよ。プログラムを書くときにやる、あれだ。
外側のテーブルの行を一つずつ取り出して、それに対応する行を内側のテーブルから探しに行く。

for row_a in table_a:
for row_b in table_b where row_a.id = row_b.a_id:
emit(row_a, row_b)

これがネステッドループの正体。データ量が少なければ、この手法はオーバーヘッドが極めて少なくて爆速なんだ。でも、テーブルが巨大化して、内側のテーブルへの検索が毎回フルスキャンになった瞬間、地獄が始まる。計算量は `(外側の行数) × (内側の行数)` になってしまうからね。

「ネステッドループ=悪」ではない理由

たまに「ネステッドループが出たらハッシュ結合に変えろ」なんて極端なアドバイスを見かけるけど、それは早計だ。

PostgreSQLのオプティマイザがネステッドループを選択するのは、「内側のテーブルへのアクセスが非常に高速である」と判断したときなんだ。具体的には、適切なインデックスが存在していて、ピンポイントでレコードを拾い出せると分かっている場合だね。

この時、ネステッドループは他の結合アルゴリズム(ハッシュ結合やマージ結合)よりも、メモリを消費せず、実行計画のオーバーヘッドも小さいため、最強の選択肢になる。

—

現場で差がつく!インデックスによる最適化のキモ

じゃあ、実際にどう最適化するのか。
例えば、`users` テーブルと、そのユーザーの `orders` テーブルを結合する場面を考えてみよう。

SELECT u.name, o.order_date
FROM users u
JOIN orders o ON u.id = o.user_id
WHERE u.status = ‘active’;

もしこのクエリでネステッドループが遅いと感じたら、まず疑うべきは「内側のテーブル(orders)に対するインデックスの欠如」だ。

1. 結合キーにインデックスを貼る(基本中の基本)

`orders` テーブルの `user_id` カラムにインデックスがないと、PostgreSQLは毎回 `orders` を全スキャンする羽目になる。

CREATE INDEX idx_orders_user_id ON orders(user_id);

これだけで、ネステッドループは「全スキャン」から「インデックスルックアップ」に変わり、劇的に速くなるはずだ。

2. 「カバリングインデックス」でトドメを刺す

さらに一歩先を目指すなら、結合先のカラムだけでなく、SELECT句で取得したいカラムも含めてインデックスを貼る「カバリングインデックス」を検討してほしい。

— order_date も含めた複合インデックス
CREATE INDEX idx_orders_user_id_date ON orders(user_id, order_date);

こうすると、PostgreSQLは `orders` テーブルのデータ本体(ヒープ領域)を見に行かなくても、インデックスのデータだけで結合と取得が完結する。「Index Only Scan」が発動すれば、I/O負荷は最小限に抑えられるんだ。

—

チューニングの際の「お約束」

最後に、一つだけアドバイスしておきたい。
クエリをいじる前に、必ず `EXPLAIN (ANALYZE, BUFFERS)` を取ってくれ。

EXPLAIN (ANALYZE, BUFFERS)
SELECT u.name, o.order_date
FROM users u
JOIN orders o ON u.id = o.user_id
WHERE u.status = ‘active’;

ここで見るべきは `loop` の回数だ。`Nested Loop` の行に表示される `actual rows` と `loops` を見てほしい。
想定外に `loops` が多い、あるいは `Index Scan` なのにコストが高い場合は、統計情報が古いせいでオプティマイザが間違った判断をしている可能性もある。そんな時は `ANALYZE` を実行して統計情報を最新にするだけで解決することもあるんだ。

—

まとめ

ネステッドループ結合は、PostgreSQLが誇る「小さく速い」ための強力な武器だ。
これを避けるのではなく、「インデックスというレールを敷いて、最短距離で走らせてあげる」のが、腕のいいエンジニアの仕事だよ。

「遅い!」と嘆く前に、まずはインデックスが正しく効いているか、余計なI/Oが発生していないかを確認する。その積み重ねが、君のデータベースを、そして君自身を強くするはずだ。

また何か詰まったら、いつでも聞いてくれ。一緒に最高に速いクエリを追い求めようぜ。

コメント

タイトルとURLをコピーしました