なぜ、PostgreSQLは「特定の値」に執着するのか?— MCVリストがクエリプランの命運を分ける理由
PostgreSQLのクエリプランナを信頼しすぎて、裏切られたことはありませんか?
例えば、特定のステータスフラグやカテゴリIDで検索したとき、明らかに「数件しかヒットしないはず」なのに、プランナがなぜかシーケンシャルスキャンを選択し、数百万行をなめるような実行計画を提示してくる。あるいは、その逆で、インデックスを使うべきところでハッシュジョインを強行し、メモリを浪費してスワップに追い込まれる……。
こうした悲劇の多くは、統計情報、特にMCV(Most Common Values:最頻値)リストの不一致に起因しています。
今回は、PostgreSQLのオプティマイザがデータ分布の「歪み」をどう捉え、そしてどう誤解するのか。その深淵を覗いてみましょう。
—
MCVリストとは何か:統計情報の「偏り」への防波堤
PostgreSQLのオプティマイザは、クエリを実行する前にコスト見積もりを行います。その際、テーブルの行数や列のカーディナリティ(値のユニーク数)を参考にしますが、均一な分布を想定するだけでは、現実はあまりに過酷です。
そこで登場するのが `pg_stats` ビューにある `most_common_vals` と `most_common_freqs` です。
- MCVリスト: テーブル内で最も頻出する値のリスト。
- MCFリスト: その値が全体に占める割合(頻度)。
これがあるおかげで、プランナは「このカラムの『1』という値は、全データの80%を占めている」といった情報を正確に把握できます。逆に言えば、このリストに含まれていない値は「その他大勢(その他すべて)」として扱われ、残りの頻度を均等割りした見積もりが適用されるわけです。
なぜ見積もりが狂うのか?
現場でよく遭遇するトラブルは、「統計情報の鮮度」と「データ分布の急激な変化」の乖離です。
例えば、あるECサイトで「未発送」ステータスのオーダーが通常は1%程度なのに、セール期間中に一時的に60%まで跳ね上がったとします。`ANALYZE` が実行されるまでの間、プランナは「未発送はレアなケース」と信じ込んでいます。その結果、インデックススキャンを選択し、結果として数百万行のRandom I/Oを引き起こしてクエリがタイムアウトする……これはDBエンジニアとして何度も見てきた「地獄」の典型例です。
現場で使えるチューニングの勘所
もしクエリの実行計画が納得いかない場合、まずは `pg_stats` を覗いてみてください。
SELECT n_distinct, most_common_vals, most_common_freqs
FROM pg_stats
WHERE tablename = ‘orders’ AND attname = ‘status’;
もし、特定のクエリで毎回プランが崩れるなら、以下の対応を検討すべきです。
1. 統計情報の粒度を上げる(ALTER TABLE … SET STATISTICS):
デフォルトの統計ターゲット(通常100)では、MCVリストが短すぎて、複雑な分布を捉えきれないことがあります。特定のカラムに対して `SET STATISTICS 1000` のように値を引き上げることで、リストの精度は劇的に向上します。ただし、`ANALYZE` の負荷とカタログの肥大化には注意が必要です。
2. 相関統計(Extended Statistics)の活用:
「AカラムとBカラムの組み合わせで頻出値が決まる」というケース(例:都道府県と市区町村)には、通常のカラム単体のMCVでは太刀打ちできません。`CREATE STATISTICS` を使い、複合的な列の相関関係をプランナに学習させましょう。これを知っているだけで、JOINの行数見積もりが劇的に改善します。
3. 定数埋め込みの罠:
アプリケーション側でクエリを構築する際、プレースホルダを使わずに値を直接埋め込んでしまうと、プランナは「パラメータ化されたクエリ」の汎用的なプランではなく、その値に特化した(しかし統計が古いと悲惨な)プランを生成することがあります。`pg_statistic` の情報は神聖ですが、万能ではないことを忘れてはいけません。
最後に:統計は「過去」の遺産である
忘れないでください。`pg_stats` は、前回の `ANALYZE` 実行時点の「歴史」に過ぎません。
どれだけチューニングを突き詰めても、急激なデータの偏りや、パターンの変化には勝てない時があります。そんな時は `pg_stat_statements` でクエリのコストを監視しつつ、必要に応じて手動での `ANALYZE` や、インデックスの再構築を検討する。この「泥臭いメンテナンス」こそが、安定したDBシステムを支える最後の砦です。
皆さんのクエリが、明日も最適化されたパスを選択してくれることを願っています。何か深掘りしたいテーマがあれば、また語り合いましょう。
コメント