クエリプランナーの「重力」を制御する:PostgreSQLコストパラメータの深淵
PostgreSQLのクエリプランナーと向き合っていると、時折「なぜこれほどまでに直感に反するプランを選択するのか」と頭を抱えたくなる瞬間がありますよね。特に、インデックスがあるのにシーケンシャルスキャン(Seq Scan)を選択したり、NESTED LOOPで済むはずの処理に無駄なハッシュ結合をねじ込んできたり……。
多くのエンジニアは、ここで `EXPLAIN ANALYZE` を叩き、インデックスの貼り直しや統計情報の更新を試みます。しかし、それらが期待通りに機能しないとき、我々が目を向けるべきは「プランナーが世界をどう認識しているか」という、コストモデルの根幹です。
今日は、PostgreSQLのクエリプランナーが用いる「重力」とも言えるコストパラメータについて、現場の視点から掘り下げてみましょう。
—
「コスト」という名の抽象概念
PostgreSQLのプランナーが算出するコストは、絶対的な実行時間ではありません。これは、いくつかの低レイヤーな操作に対する「相対的な重み付け」の合計値です。
- `seq_page_cost`: 1ページを順次読み込むコスト(デフォルト 1.0)
- `random_page_cost`: 1ページをランダムアクセスで読み込むコスト(デフォルト 4.0)
- `cpu_tuple_cost`: 1行を処理するコスト(デフォルト 0.01)
- `cpu_index_tuple_cost`: インデックス内で1行を処理するコスト(デフォルト 0.005)
ここで重要なのは、これらのデフォルト値が「HDD(ハードディスク)」という、物理的にヘッドを動かす必要があった時代のレガシーを色濃く引き継いでいるという点です。
現代のハードウェアとの「乖離」を埋める
皆さんが運用しているデータベースは、今やNVMe SSDやクラウド上の高速なブロックストレージの上で動いているはずです。この環境において、`random_page_cost` が `seq_page_cost` の4倍も高いというのは、明らかに不自然です。
もし `random_page_cost` をデフォルトの4.0のまま運用していると、プランナーは「インデックススキャンはコストが高い」という誤った判断を下し続け、本来ならインデックスで高速に引けるはずのクエリに対して、律儀にフルテーブルスキャンを選びます。
現場で私が最初に行うチューニングの一つが、この値を下げることです。
— SSD環境であれば、1.1〜1.2程度まで下げるのが定石
ALTER SYSTEM SET random_page_cost = 1.1;
SELECT pg_reload_conf();
これだけで、プランナーは「ランダムアクセスは安価である」という現代の物理特性を理解し、よりインデックスを積極的に活用する賢いプランを選択するようになります。
プランナーを迷走させる「コストの罠」
しかし、パラメータをいじれば全てが解決するわけではありません。ここで気をつけたいのが、`cpu_tuple_cost` などのCPU系パラメータです。
複雑なJOINや、大量のフィルタリングを伴うクエリにおいて、もしプランナーがインデックスを選択しすぎると、かえって「ランダムアクセスのオーバーヘッド」がCPU負荷を押し上げるケースがあります。
特に、テーブルサイズがメモリキャッシュ(shared_buffers)に収まりきらない境界線にあるとき、プランナーの推論は非常に不安定になります。プランナーは「データが全てディスク上にある」という最悪のケースを想定してコスト計算を行うため、実際にはメモリ上で完結しているような高速なクエリに対しても、非常に保守的なプランを選択することがあるのです。
トラブルシューティングの極意:観測することから始める
「遅いクエリ」に直面したとき、闇雲にコストパラメータをいじり回すのは、目隠しで手術をするようなものです。まずは以下の手順でプランナーの認識を可視化してください。
1. `EXPLAIN (ANALYZE, BUFFERS)` を活用する:
プランが実際にどれだけのページを読み込み、どれだけがキャッシュ(Shared Read/Hit)にヒットしているかを確認してください。
2. `pg_stat_user_tables` で統計情報の鮮度を疑う:
コスト計算の根幹である「行数推定(Cardinality Estimation)」が狂っている原因のほとんどは、統計情報の陳腐化です。`autovacuum` が追いついていないテーブルには、手動での `ANALYZE` が劇薬となります。
3. 特定のクエリだけを制御する:
サーバー全体の設定(`postgresql.conf`)をいじるのが怖い場合は、セッション単位で一時的にパラメータを適用して挙動の変化を見てください。
SET LOCAL random_page_cost = 1.0;
— この状態でEXPLAINを叩き、プランの変化を観察する
最後に:プランナーは「敵」ではなく「良きパートナー」
PostgreSQLのクエリプランナーは、非常に高度で、かつ慎重なアルゴリズムです。彼らが奇妙なプランを出すとき、それは往々にして「我々人間が統計情報を更新していなかった」か、「インデックスの構成がデータ特性と合致していない」かのどちらかです。
コストパラメータの最適化は、SQLの魔術師になるための第一歩です。デフォルト値に盲従せず、自らのインフラの特性をプランナーに教えてやる。その対話こそが、データベースエンジニアとしての醍醐味ではないでしょうか。
さて、皆さんの本番環境では、今日もプランナーは期待通りのプランを出してくれていますか?もし裏切られているなら、まずは `random_page_cost` を少しだけ弄ってみることから始めてみてください。世界が変わるはずです。
コメント