「PostgreSQLのクエリがなぜか遅い……インデックスも貼っているのに、なんでフルスキャン(Seq Scan)してるんだ?」
現場でこんな悲鳴を聞くと、僕は決まって「プランナーの設定値、ちゃんと見てる?」と聞くことにしているんだ。
PostgreSQLのオプティマイザは非常に賢いけれど、「何をもってコストが高いとみなすか」という価値観は、デフォルトのままだと現代のハードウェア環境とズレていることがよくある。今日は、クエリプランナーの裏側を覗いて、君のデータベースを「今の環境」に最適化させるための話をしよう。
—
なぜ「コストパラメータ」をいじる必要があるのか?
PostgreSQLのプランナーは、実行計画を立てる際に「このクエリを実行するのにどれくらいのコストがかかるか」を数値化して比較している。その計算式で使われているのが、`postgresql.conf` にある一連のコストパラメータだ。
特に重要なのが、ストレージへのアクセスに関するこの二つだ。
- `seq_page_cost`: シーケンシャルアクセスで1ページ読み込むコスト(デフォルト: 1.0)
- `random_page_cost`: ランダムアクセスで1ページ読み込むコスト(デフォルト: 4.0)
見ての通り、デフォルト設定では「ランダムアクセスはシーケンシャルの4倍コストがかかる」という前提になっている。これはHDD(ハードディスク)全盛時代の名残だ。物理的なヘッドの移動が必要なHDDなら、ランダムアクセスは確かに重い。
でも、今のインフラがSSDやNVMeならどうだろう?
読み込みのコスト差は、HDDほど大きくないはずだよね。ここがズレていると、プランナーは「インデックスを使うより、いっそ全部読み込んだ方が速いんじゃね?」と勘違いして、無駄なフルスキャンを選んでしまうことがある。
—
実践:どうやってチューニングするか
まず、今の設定を確認してみよう。
SHOW seq_page_cost;
SHOW random_page_cost;
もし君の環境がクラウドの高速なSSDストレージなら、`random_page_cost` を 1.1 〜 1.2 くらいまで下げてみるのが定石だ。
設定変更のステップ
まずはセッション単位で試すのがエンジニアの作法だね。
— セッション単位で変更してクエリの挙動を確認
SET random_page_cost = 1.1;
EXPLAIN ANALYZE SELECT FROM users WHERE id = 12345;
これで計画が「Seq Scan」から「Index Scan」に変わるなら、プランナーが本来のハードウェア性能を理解できていなかった証拠だ。
—
もう一つの隠れた主役:cpu_tuple_cost
忘れられがちだけど、めちゃくちゃ効いてくるのが `cpu_tuple_cost` だ。これは「1行(タプル)を処理するコスト」なんだけど、これが大きすぎると、プランナーは「行数が多いテーブルをスキャンするのは避けよう」と、極端にインデックスを好むようになる。
逆に、複雑なJOINや集計を多用する分析系クエリが多い環境では、ここを少し調整することでプランナーの判断が劇的に改善することがある。
- `cpu_tuple_cost` (デフォルト: 0.01)
- メモリ上で複雑な計算やフィルタリングをたくさんする場合、ここをいじると「インデックスを貼ったけど、結局全件スキャンしたほうが速いよね」というプランナーの妥当な判断を引き出しやすくなる。
—
先輩からのアドバイス:闇雲にいじらないこと
ここまで読んで「じゃあ全部下げればいいんだな!」と思った君、ちょっと待って。
コストパラメータは「バランス」が全てだ。
特定のクエリを速くしようとして極端な値を設定すると、別の重要なクエリでプランナーが迷走し、システム全体が崩壊するリスクがある。
1. まずは `EXPLAIN (ANALYZE, BUFFERS)` を見る: どこで時間がかかっているのか、本当にインデックスが使われていないのかを確信すること。
2. `random_page_cost` から着手する: SSD環境なら、まずはここを 1.1 に寄せるだけで十分な効果が出ることが多い。
3. 統計情報を疑う: プランナーが誤る最大の原因は、実はパラメータではなく「古い統計情報」であることが大半だ。「`ANALYZE;` を打ったら直った」というのは、現場あるある中のあるあるだよ。
—
最後に
データベースエンジニアの仕事は、魔法をかけることじゃなくて、プランナーに「今のハードウェアはこれくらい優秀なんだよ」という正しい現実を教えてあげることだ。
設定を変更したら、必ず本番に近いデータ量と負荷で検証してほしい。もし「設定を変えたのに変わらない!」という時は、設定値の反映漏れ(再起動やセッションの張り直し)や、`autovacuum` が追いついていない可能性を疑ってみて。
また何か詰まったら聞きに来てくれ。データベースの深淵を一緒に攻略しようじゃないか。
コメント