「なぜPostgreSQLはあんな実行計画を選んだのか?」――コストベース最適化の裏側を覗く
現場で働いていると、必ず一度は遭遇するよね。「インデックスを貼ったはずなのに、なぜかフルスキャンしてるぞ?」とか、「このクエリ、なんでこんなに遅いんだ?」っていう悩み。
チューニングの泥沼にはまったとき、多くのエンジニアはとりあえず `EXPLAIN ANALYZE` を叩く。でも、そこに表示される「コスト(Cost)」の数値を、ただの「なんとなくの重さ」として捉えていないかな?
今日は、PostgreSQLがどうやってその「コスト」を計算し、実行計画を選んでいるのか、その頭の中をちょっと覗いてみよう。これを知っているだけで、データベースを見る目がガラリと変わるはずだ。
—
1. コスト見積もりの正体:CPUとI/Oの「見積もり計算」
PostgreSQLのオプティマイザは、コストベース・オプティマイザ(CBO)と呼ばれる仕組みで動いている。
簡単に言えば、「このクエリを処理するのに、ディスクをどれだけ読みに行くか(I/Oコスト)」と「データをソートしたり関数を計算したりするのに、どれだけCPUを使うか(CPUコスト)」を合算して、もっともコストが低いルートを選んでいるんだ。
このとき、判断材料にするのが `pg_statistic` というシステムカタログに格納された「統計情報」だ。
- 行数(n_tuples): テーブル全体に何行あるか。
- 列の分布(histograms / mcv): データがどれくらい偏っているか。
- 相関(correlation): ディスク上の物理的な並び順とインデックスの並びがどれくらい一致しているか。
もし「統計情報が古い」と、この計算が狂う。現実は数百万行あるのに「たった10行しかない」と見積もられたら、オプティマイザは迷わずインデックスを捨ててフルスキャンを選ぶ。これが、いわゆる「統計情報の鮮度不足による悲劇」の正体だ。
—
2. 実践:コストの「見える化」と「いじり方」
じゃあ、実際にコストをどう意識するか。まずは、クエリのコストがどう構成されているかを確認してみよう。
— 基本の実行計画表示
EXPLAIN (COSTS, VERBOSE)
SELECT FROM orders WHERE user_id = 12345;
ここで表示される `cost=0.00..8.27` という数値。
- 0.00: 最初の1行目を取得するまでの開始コスト(スタートアップコスト)。
- 8.27: すべての行を取得し終えるまでの総コスト。
もし、ここでのコスト見積もりが実際と乖離しているなら、`postgresql.conf` で調整できるパラメータに目を向ける必要がある。現場でよく触るのはこのあたりだ。
- seq_page_cost (デフォルト: 1.0): シーケンシャルスキャン(フルスキャン)のコスト。
- random_page_cost (デフォルト: 4.0): インデックス経由など、ランダムアクセス時のコスト。
「SSDを使っているのに、デフォルトの4.0のままにしていない?」
最近の高速なストレージ環境なら、この `random_page_cost` を `1.1` くらいまで下げてみると、オプティマイザが「ランダムアクセスも悪くないじゃん」と判断して、インデックスを積極的に使ってくれるようになることが多いんだ。
—
3. 先輩からのアドバイス:オプティマイザを信じすぎない
最後に、一つだけ。PostgreSQLのオプティマイザは非常に優秀だけど、完璧じゃない。
たまに、いくら統計情報を更新しても、どうしても変な実行計画から抜け出せないことがある。そんなときは、「統計情報を疑う」のと同時に、「クエリそのものを疑う」ことも忘れないでほしい。
例えば、WHERE句で関数を使ったりしていないかな?
— これだと統計情報が使えないことがある
SELECT FROM orders WHERE DATE(created_at) = ‘2023-10-01’;
これだと、`created_at` にインデックスがあっても、計算結果に対して比較が行われるため、オプティマイザは「列の統計情報」をうまく使えない。
— こっちなら統計情報をフル活用できる
SELECT FROM orders WHERE created_at >= ‘2023-10-01’ AND created_at < '2023-10-02';
コスト計算の仕組みを理解すると、「なぜPostgreSQLがそのルートを選んだのか」という理由が論理的に見えてくる。そうすれば、当てずっぽうのインデックス追加や、闇雲なチューニングから卒業できるはず。
データベースとの対話は、まさに「PostgreSQLの脳内シミュレーション」を自分の中に再現することから始まるんだ。ぜひ、次のチューニングの機会に試してみてくれ。
---
今日のまとめ:
1. コストは「統計情報」に基づいた「予測値」に過ぎない。
2. `EXPLAIN` を見て、見積もりと実際の行数(`rows` vs `actual rows`)がズレていないか確認せよ。
3. ハードウェア環境に合わせて `random_page_cost` を見直す勇気を持つこと。
また次の記事で会おう!質問があればコメント欄まで。
コメント