やあ。また会ったね。今日は少しだけ「PostgreSQLの深淵」を覗いてみようか。
現場でバリバリコードを書いていると、ふとクエリが遅いことに気づいて `EXPLAIN` を叩く瞬間ってあるよね。でも、そこで出力された膨大なテキストを見て、「ふーん、なんか色々やってるな」で終わらせていないかい?
もしそうなら、それは非常にもったいない。`EXPLAIN` の出力結果である「実行計画ツリー」を読めるようになることは、データベースエンジニアとしての武器を一つ増やすのと同義なんだ。
今日は、この「実行計画ツリー」というパズルをどう解き明かすか、一緒に見ていこう。
—
実行計画ツリーは「料理のレシピ」だと思えばいい
PostgreSQLがクエリを受け取ると、オプティマイザが「一番効率よくデータを集めるにはどうすればいいか?」を必死に考えて、一つの「計画書」を作る。それが実行計画だ。
これは単なる命令の羅列じゃない。「ツリー構造」をしているんだ。
ツリーの末端(葉)にあるノードが、ディスクからデータを拾い上げる最初の一歩。そして、その結果が親ノードに渡され、結合されたりソートされたりしながら、最終的に君たちの画面に結果が返ってくる。
イメージとしては、下から上へ流れる「料理の調理工程」だ。
具体的なツリーを読んでみよう
例えば、こんなクエリを考えよう。
SELECT u.name, o.amount
FROM orders o
JOIN users u ON o.user_id = u.id
WHERE o.amount > 1000
ORDER BY o.amount DESC;
これに対して `EXPLAIN (ANALYZE, BUFFERS)` を叩くと、こんな感じのツリーが出てくるはずだ。
Sort (cost=…) (actual time=…)
-> Hash Join (cost=…)
-> Seq Scan on orders o
-> Hash (cost=…)
-> Seq Scan on users u
現場で見るべき「ノード」の勘所
初心者はついコストの数字ばかり見がちだけど、現場の僕たちがまず見るのは「ツリーの形」と「ノードの種類」だ。
- Scan系(Seq Scan, Index Scan):
一番下のノードだ。ここが「フルスキャン」になっていないかを確認する。データ量が多いテーブルで `Seq Scan` が出ているなら、まずはインデックスを疑うのが定石だね。
- Join系(Hash Join, Nested Loop, Merge Join):
ここがボトルネックになることが多い。`Nested Loop` は小さなテーブル同士なら爆速だけど、数百万行の結合で選ばれると地獄を見る。逆に、`Hash Join` はメモリを食うけど大量データには強い。
- Sort:
こいつは要注意だ。「メモリでソートしきれずディスクに溢れていないか?」をチェックする必要がある。`work_mem` の設定が適切じゃないと、ここで一気にクエリが遅くなるんだ。
エンジニアとして「ツリーを操作する」技術
ただ眺めるだけじゃなくて、ツリーを変える力も必要だ。
もし実行計画が意図しない方向に進んでいるなら、オプティマイザにヒントを与える必要がある。例えば、「このテーブルは必ずインデックスを使ってほしい」という時は、こんなふうに制御することもあるよね。
— 一時的にNested Loopを強制して挙動を見る(※本番運用では慎重に!)
SET enable_hashjoin = off;
EXPLAIN …
でも、一番大事なのは「なぜオプティマイザがそのツリーを選んだか」を想像すること。統計情報が古いせいで、行数を過小評価していないか? `ANALYZE` を忘れていないか?
「機械が考えた計画を、人間が推敲する」。これがチューニングの醍醐味だよ。
—
最後に:完璧なクエリなんてない
たまに「一番綺麗な実行計画はどう書くべきか?」と聞かれることがあるけれど、答えは「その時のデータ次第」としか言えない。
データが1,000件の時と、1億件の時では、最強の実行計画は全く別物になる。だからこそ、僕たちは「今、このクエリがどういうツリーを描いているか」を常に監視し、変化に対応できるようにしておく必要があるんだ。
次に遅いクエリに出会ったら、焦らず `EXPLAIN` のツリーをじっくり眺めてみてほしい。ノードの一つ一つが、実は君の命令を必死に実行している「小さな働き者」に見えてくるはずさ。
もし読み解きで詰まったら、いつでも相談してくれ。データベースの深淵は、意外と面白い景色が広がっているよ。
それじゃ、また現場で会おう。
コメント