なぜ、NULLの割合がクエリプランを狂わせるのか:PostgreSQLの統計情報の深淵
現場で泥沼のパフォーマンストラブルに直面したとき、多くのエンジニアはまずインデックスの有無や `EXPLAIN ANALYZE` のコストを疑います。もちろんそれは正しいアプローチですが、もし「なぜかプランナがとんでもない結合順序を選択している」という事態に陥ったなら、一度 `pg_stats` の奥底を覗いてみるべきです。
特に、意外と軽視されがちなのが `null_frac` です。ただの「NULLの割合」と侮るなかれ。このわずかな数字が、クエリの実行計画を大きく歪ませるトリガーになることが多々あるのです。
null_frac がプランナの判断を変える瞬間
PostgreSQLのコストベースオプティマイザ(CBO)は、クエリを実行する前に「この条件に合致する行はどれくらいあるか(選択率)」を予測します。このとき、プランナは統計情報カタログである `pg_stats` を参照します。
例えば、`WHERE column_a IS NOT NULL` という条件があるとき、プランナは単なる直感ではなく、`null_frac` を使って次のように計算します。
— 選択率の簡易モデル
選択率 = 1.0 – null_frac
もし、統計情報が古く、`null_frac` が実際のデータと大きく乖離していたらどうなるか。プランナは「ほとんどの行がNULLだ(あるいはその逆)」と勘違いし、結果として不要な全件スキャンを選択したり、Nested Loopのループ回数を極端に見誤って悲惨なパフォーマンスを引き起こしたりします。
現場で遭遇した「統計情報の罠」
以前、数億行規模のテーブルで「特定の列がNULLではないレコードだけを集計する」というクエリが、インデックスがあるにもかかわらず全件スキャン(Seq Scan)を選択し続けるという問題に遭遇しました。
原因は、データの投入プロセスにありました。バッチ処理で大量のデータを流し込んだ直後、`ANALYZE` を実行する前にクエリが走っていたのです。
- 発生していたこと: `null_frac` が古い統計値のまま固定され、実際にはNULLがほぼ皆無の列に対して「かなりの確率でNULLが含まれているはずだ」という誤った推定がなされていた。
- 結果: プランナは「この条件で絞り込んでも大して行数は減らないだろう」と判断し、コストの低いインデックススキャンを捨てて、巨大なテーブルのSeq Scanを選択していた。
このとき痛感したのは、「NULLの分布は、データのライフサイクルと密接に関係している」ということです。特に、後から値が埋められるようなカラム(例:ステータス管理フラグや処理日時)において、`null_frac` は刻一刻と変動します。
トラブルシューティングの勘所
もし皆さんの環境で「妙なプラン」が出ているなら、まずは以下の手順で統計を確認してみてください。
1. pg_stats を直視する
まずは、問題のカラムの統計を疑います。
SELECT null_frac, n_distinct, most_common_vals
FROM pg_stats
WHERE tablename = ‘your_table’ AND attname = ‘your_column’;
ここで、`null_frac` が `0` なのか `1` なのか、あるいは現実離れした数値になっていないかを確認します。
2. インデックスの「NULL値」を考慮する
これは意外な落とし穴ですが、PostgreSQLのB-treeインデックスは、デフォルトでNULL値を含みません。`WHERE column IS NULL` というクエリを高速化したい場合、通常のインデックスでは役に立たず、インデックスが無視されることになります。
このようなケースでは、「部分インデックス(Partial Index)」が特効薬になります。
CREATE INDEX idx_nullable_col_is_null
ON your_table (column_a)
WHERE column_a IS NULL;
こうすることで、NULL値だけを効率的に抽出できる軽量なインデックスが生成されます。`null_frac` が高い(=NULLが多い)カラムを頻繁に検索対象にするなら、検討の価値は十分にあります。
最後に:統計は「生き物」である
データベースエンジニアとして長く仕事をしていると、「統計情報は一度作ったら終わり」ではないことを骨身に染みて理解します。
アプリケーションの仕様変更で「NULLを許容するカラム」が増えたり、あるいはデータの傾向が変わったりするたびに、データベースの「世界観(=統計情報)」と「現実」の間にズレが生じます。`null_frac` は、そのズレを測るための非常に重要なバロメーターです。
もし次にクエリチューニングで行き詰まったら、`EXPLAIN` の数字を眺めるだけでなく、データそのものがプランナにどう「見えているのか」を想像してみてください。その視点を持つだけで、ボトルネックの解決スピードは格段に上がるはずです。
それでは、良いチューニングライフを。
コメント