「それ、統計情報のせいかも?」PostgreSQLのn-distinct係数で複雑なクエリを爆速にする話
現場でクエリを書いていて、「明らかにインデックスが効くはずなのに、なぜかSeq Scanが選ばれる」「見積もりが極端にズレていて、結合順序がめちゃくちゃになっている」なんて経験、一度はあるよね。
特に、`GROUP BY` を多用する分析クエリや、複数のカラムを組み合わせてフィルタリングするような場面で、PostgreSQLのオプティマイザが迷子になることは珍しくない。
今日は、そんな悩みを解決する隠し玉、「拡張統計(Extended Statistics)」のn-distinct係数について、実務的な視点で深掘りしてみようと思う。
—
なぜオプティマイザは「勘違い」をするのか
まず、PostgreSQLのデフォルトの挙動を思い出してほしい。通常、統計情報はカラムごとに独立して収集されるよね。
例えば、あるECサイトの注文テーブルで「都道府県(prefecture)」と「市町村(city)」というカラムがあるとしよう。オプティマイザはそれぞれの一意な値の数は知っているけど、「この県にはこの市がある」という列間の相関関係までは知らないんだ。
結果として、オプティマイザは「都道府県と市を組み合わせたユニークな値の数」を過大評価(あるいは過小評価)し、コスト計算をミスる。これが、実行計画が最適化されない根本的な原因だ。
救世主:n-distinct統計の出番
ここで登場するのが `CREATE STATISTICS` だ。これを使うと、PostgreSQLに「この列とこの列はセットで見てくれ」と指示を出せる。特に `n_distinct` オプションを使えば、組み合わせのユニーク数を正確に把握させることができるんだ。
実践:どう設定すればいいのか?
例えば、`orders` テーブルで `(category_id, sub_category_id)` という組み合わせが頻出するなら、以下のように設定する。
CREATE STATISTICS stats_category_sub
ON category_id, sub_category_id
FROM orders;
これだけで、PostgreSQLは内部的にこの2列の組み合わせに関する統計情報を収集し始める。ただし、作成しただけでは統計情報は生成されない。忘れずに `ANALYZE` を叩こう。
ANALYZE orders;
—
どんなときに劇的な効果があるのか
この統計が真価を発揮するのは、「複数のカラムでGROUP BYをしているのに、実行計画が想定と違うとき」だ。
例えば、こんなクエリを考えてみて。
SELECT category_id, sub_category_id, COUNT()
FROM orders
GROUP BY 1, 2;
この時、もしオプティマイザが「組み合わせのユニーク数」を正しく見積もれていないと、ハッシュ集約(HashAggregate)をすべきところで、無駄にソート(GroupAggregate)を選んでメモリを圧迫したりする。
n-distinct統計を入れておくと、この「組み合わせのカーディナリティ(一意性の度合い)」が正確に計算されるから、オプティマイザは「あ、これならハッシュテーブルに載せてもメモリが溢れないな」と、自信を持って最適なプランを選択できるようになるんだ。
先輩からの注意点:魔法の杖ではない
これ、便利だからといって全テーブルの全組み合わせに適用するのはNGだよ。
1. オーバーヘッド: 統計情報の収集にはCPUリソースを使う。頻繁に更新されるテーブルで、意味のない組み合わせにまで統計を張ると、`ANALYZE` の時間が長引いてしまう。
2. 目的を見極める: 単一カラムのクエリや、JOINでしか使わないカラムに対しては、デフォルトの統計で十分なケースが多い。あくまで「見積もりがズレている」という確信があるクエリに対してピンポイントで投入するのがプロのやり方だ。
まとめ
実務でクエリチューニングをするとき、`EXPLAIN ANALYZE` を叩いて「見積もり行数(rows)」と「実際の行数(actual rows)」を比較するのは基本中の基本だよね。そこで大きな乖離を見つけたら、まずはこの「拡張統計」を思い出してほしい。
「統計情報を最適化する」ということは、オプティマイザに正しい地図を渡すようなものだ。これだけで、劇的にレスポンスが改善することは現場でも本当によくある。
ぜひ、次のチューニング案件で試してみて。もし「こんなパターンで効いたよ!」なんて経験があったら、ぜひ教えてほしい。エンジニア同士、知見を共有していこうぜ!
コメント