「統計情報の罠」とサンプリングの真実:PostgreSQLのANALYZEを再考する
PostgreSQLを長年触っていると、誰しも一度は「なぜオプティマイザはこんなとんでもない実行計画を立てたのか?」と頭を抱えた経験があるはずです。その原因の多くは、統計情報が実態と乖離していることにあります。
特に数億行を超えるような大規模テーブルにおいて、`ANALYZE`は諸刃の剣です。テーブル全体をスキャンすれば正確な統計が取れますが、本番環境のIO負荷は無視できない。かといってデフォルト設定のままでは、サンプリングの精度が低すぎて、オプティマイザが迷走する。
今日は、この「統計情報のサンプリング」という、地味だけど極めて重要な領域について、少し深掘りしてみたいと思います。
—
なぜ「フルスキャン」を避けるのか?
PostgreSQLの`ANALYZE`は、デフォルトでは`default_statistics_target`に基づいた一定の割合をランダムにサンプリングしてヒストグラムやMCV(Most Common Values)を構築します。
ここでエンジニアが意識すべきは、「サンプリングは魔法ではない」ということです。
PostgreSQLは、テーブルのページをランダムに読み込みます。これは、データが物理的にどう配置されているか(クラスター化されているか)に強く依存します。もし、インデックスの順序とデータの挿入順序が全く異なるような状況で、サンプリングサイズを小さくしすぎると、特定のキーに偏ったデータが「統計情報の盲点」となって現れます。
統計情報の内部構造:なぜ「ターゲット」が重要なのか
`default_statistics_target`(デフォルトは100)という設定値は、具体的に何をしているのか。これは単なる「何行読むか」という回数ではありません。
- MCVリストの最大サイズ
- ヒストグラムのバケット数
この2つを制御するパラメータです。つまり、ここを大きくすれば統計情報の解像度が上がりますが、その分、`ANALYZE`実行時のCPU負荷と、カタログ情報(`pg_statistic`)の容量が増大します。
経験則ですが、カラムのカーディナリティ(値の種類の多さ)が極端に高い場合や、データ分布が歪んでいる場合には、この値をデフォルトのまま放置するのは危険です。「このカラムは結合条件によく使われるのか?」「WHERE句で範囲指定されることが多いのか?」を考え、カラムごとに`ALTER TABLE … ALTER COLUMN … SET STATISTICS`で個別にチューニングするのが、プロのエンジニアの嗜みというものです。
パフォーマンストラブルシューティングの勘所
もしクエリが遅いと感じた時、私はまず`pg_stats`を覗きます。特に注目するのは以下の点です。
1. correlation(相関): 物理的な行の順序とカラムの順序の相関。これが1に近いか-1に近いかによって、インデックススキャンのコスト見積もりが劇的に変わります。
2. n_distinct: 推定値が実際のカーディナリティと乖離していないか。ここがずれていると、Nested Loopの回数見積もりが崩壊し、悲惨な実行計画が生成されます。
もしここで違和感を覚えたら、迷わず`EXPLAIN ANALYZE`の「Actual Rows」と「Estimated Rows」を比較してください。見積もりが大きく外れている場合、それは統計情報のサンプリングが失敗しているか、あるいは統計情報が陳腐化しているサインです。
サンプリング手法の限界をどう超えるか
大規模テーブルにおいて、サンプリング精度を上げようとすると`ANALYZE`が重くなりすぎるというジレンマがあります。最近のPostgreSQLでは統計情報の収集も進化していますが、それでも「どうしても正確な分布が必要」なケースはあります。
そんな時、私は以下の戦略を取ることがあります。
- 統計情報の強制更新: 特定のバッチ処理の直後に明示的に`ANALYZE`を走らせる。
- サンプリング対象を絞る: 全カラムを一律で扱うのではなく、クエリの主要なキーとなっているカラムのみ`STATISTICS`の値を上げる。
- pg_stats_ext: 複数カラム間の相関が激しい場合(例:都道府県と市区町村)、単一カラムの統計だけでは不十分です。拡張統計(Extended Statistics)を利用して、カラム間の依存関係をオプティマイザに教え込む。
最後に:データベースと対話するということ
統計情報は、データベースが「自分の持っているデータ」を理解するための唯一の手段です。
多くのエンジニアは実行計画という「結果」ばかりに目を向けがちですが、その裏側にある「統計情報がどう生成されたか」というプロセスを理解すると、PostgreSQLとの対話がより深く、楽しくなります。
「なんとなく遅いから`ANALYZE`を打つ」のではなく、「このカラムのカーディナリティを正確に把握させるために、あえて統計ターゲットを300に引き上げる」といった意図ある設定。これこそが、大規模システムを安定稼働させるためのエンジニアリングだと私は信じています。
皆さんの現場でも、一度`pg_stats`の奥深くに潜り込んでみてはいかがでしょうか。そこには、クエリが遅くなる明確な理由が眠っているはずですから。
コメント