オプティマイザの「目」を曇らせるな:PostgreSQLのヒストグラムと統計情報の深層
データベースを運用していると、誰もが一度は直面する悪夢があります。
「なぜ、このクエリだけインデックスがあるのにシーケンシャルスキャンを選ぶんだ?」
「なぜ、見積もり件数が1行なのに、実際には数百万行も返ってくるんだ?」
Explain Analyzeの結果を見て頭を抱えた経験があるなら、その原因の多くはオプティマイザの「予測のズレ」にあります。そして、その予測精度の要(かなめ)となっているのが、今回掘り下げる「ヒストグラム(Histogram)」という統計情報です。
統計情報の「解像度」を理解する
PostgreSQLのオプティマイザは、コストベースの最適化を行います。つまり、「どのプランが最もコストが低いか」を計算するわけですが、その計算式には「このクエリが何行をヒットさせるか(selectivity)」という値が不可欠です。
`pg_stats` を覗いたことはありますか?そこにある `histogram_bounds` こそが、PostgreSQLがテーブルのデータ分布を理解するために持っている「低解像度の地図」です。
デフォルトでは、各カラムに対して100個のビン(バケット)が用意されます。これは全データを100等分するのではなく、「データが等しい個数ずつ含まれるように区切った境界値」です。つまり、値の密度が高い領域は細かく、低い領域は粗く表現されます。この仕組みによって、不均一なデータ分布に対しても、ある程度の精度を確保しているわけです。
なぜ「範囲検索」で見積もりが暴れるのか
ここで一つ、高度なチューニングの現場で頻発する罠についてお話しします。
等価比較(`=`)であれば、`most_common_vals`(MCVリスト)を使ってピンポイントに予測できます。しかし、`BETWEEN` や `>`, `<` といった範囲検索になった途端、オプティマイザはヒストグラムを頼りに「補間」を行います。 問題は、「ヒストグラムの境界値の間の分布は均一である」という仮定に基づいていることです。
もし、あなたのカラムに「特定の範囲にデータが密集しているが、ヒストグラムの境界を跨いでいる」ようなデータがあれば、オプティマイザは境界内を単純な線形補間で計算してしまいます。結果として、現実とはかけ離れた見積もりが生成され、Nested Loopを選ぶべきところでHash Joinが選択される……といった悲劇が生まれます。
現場で使える「処方箋」
もし、特定のクエリの実行計画がどうしても改善しない場合、以下のステップを検討してみてください。
- 統計情報の解像度を上げる (`ALTER TABLE … SET STATISTICS`)
デフォルトの100という値は「標準」に過ぎません。特定のカラムに対してのみ統計情報の解像度を上げることができます。例えば、500や1000に引き上げることで、より緻密なヒストグラムを生成させることが可能です。ただし、分析(`ANALYZE`)のコストと、統計情報が保持されるメモリ消費量が増えることは忘れないでください。
- 相関関係の考慮 (`CREATE STATISTICS`)
これが最も盲点になりやすいポイントです。ヒストグラムはカラム単体での分布しか見ません。「カラムAとカラムBの間には強い相関がある」といった関係性は、通常の統計情報では無視されます。もし `WHERE a = x AND b = y` のようなクエリが遅いなら、`CREATE STATISTICS` で拡張統計情報を作成し、多変量統計をオプティマイザに教え込む必要があります。
最後に:魔法の杖はない
私が若手のエンジニアによく言うのは、「プランナを信じるな、だが理解せよ」ということです。
ヒストグラムをいじり回してチューニングする行為は、いわば「オプティマイザの視力矯正」です。しかし、根本的なデータ設計やクエリの書き方そのものが悪ければ、どれだけ統計を整えても、いつか必ず別のクエリでボロが出ます。
PostgreSQLは非常に賢いデータベースですが、それでも「何を知らないか」を隠し持っています。その隠れた領域を `pg_stats` や `EXPLAIN` を通じて暴き出し、オプティマイザと対話する。これこそが、データベースエンジニアの醍醐味であり、真のパフォーマンスを引き出す唯一の近道だと私は信じています。
皆さんのデータベースのヒストグラムは、正しく世界を映し出していますか?ぜひ一度、`pg_stats` を開いてみてください。そこには、クエリを速くするためのヒントが必ず眠っています。
コメント