「なぜかクエリが遅い」を解決する、PostgreSQLの『ANALYZE』との付き合い方
現場でデータベースを触っていると、必ず一度はぶち当たる壁があります。「さっきまで爆速だったクエリが、急に重くなった」という現象です。
原因の多くは、PostgreSQLのクエリプランナが「データの中身」を誤解していることにあります。そんなとき、魔法の杖のように効くのが `ANALYZE` コマンドです。今日は、教科書には載っていない「現場でのANALYZEの勘所」について、少し話をさせてください。
そもそも、PostgreSQLは「勘」で動いている
まず前提として、PostgreSQLのプランナは全知全能ではありません。彼らは `pg_statistic` という「統計情報」という名のカンニングペーパーを見ながら、「どのインデックスを使って、どの順番でテーブルを結合するか」を考えています。
もし、このカンニングペーパーが古かったらどうなるでしょうか?
- 「このテーブルには100件しかデータがないはずだ」と信じ込んでいるのに、実際には1,000万件に増えている。
- データの分布が偏っているのに、均等に散らばっていると誤解している。
結果として、プランナは「全テーブルスキャンで十分だろ」という、最悪の実行計画を選んでしまうわけです。これを正すのが `ANALYZE` の役割です。
ANALYZEを打つべき「タイミング」
「じゃあ、毎日cronで全テーブルANALYZEすればいいのでは?」と思うかもしれません。でも、それはおすすめしません。`ANALYZE` はテーブルをスキャンするコストがかかるため、頻繁にやりすぎるとDBに余計な負荷をかけてしまいます。
僕が現場で `ANALYZE` を手動で叩くのは、主に以下のようなケースです。
1. バッチ処理で大量データを投入した直後
- 100万件単位のINSERTやDELETEをした後は、統計情報が激しくズレます。バッチの最後に「お疲れ様」のつもりで `ANALYZE` を叩くのが鉄則です。
2. 実行計画が明らかに不自然なとき
- `EXPLAIN ANALYZE` を叩いてみて、「Actual Rows(実際の行数)」と「Estimated Rows(予測行数)」が桁違いにズレているなら、迷わず実行です。
3. WHERE句の条件が特定のデータに偏ったとき
- 特定のステータスだけ異常に多い、といったデータ分布の変化があったときも有効です。
具体的な使い方とコード例
使い方は非常にシンプルですが、現場では範囲を絞ることが重要です。
1. テーブル全体を更新する
一番基本ですね。対象テーブルを指定するだけです。
ANALYZE users;
2. 特定の列だけに絞って更新する
特定の列にインデックスを貼った直後や、その列の検索条件が頻繁に変わる場合は、列指定で負荷を下げられます。
ANALYZE users (last_login_at, status);
3. バッチ処理の最後に入れる例
例えば、PythonやGoのバッチ内でトランザクションを閉じた直後にこう書くのが、僕の定番スタイルです。
— 大量更新処理
BEGIN;
DELETE FROM logs WHERE created_at < '2023-01-01';
COMMIT;
-- 統計をリフレッシュしてプランナを正気に戻す
ANALYZE logs;
ここがプロのポイント:オートバキュームを信じすぎない
「PostgreSQLにはオートバキューム(autovacuum)があるから大丈夫じゃないの?」という声も聞こえてきそうです。確かにその通り。バックグラウンドで勝手に `ANALYZE` も走っています。
でも、オートバキュームが走るまで待てない時があるんです。
例えば、深夜のバッチ処理で巨大なテーブルを掃除した直後、朝一番のユーザーアクセスが急増する時間帯に「統計が古いまま」だと、朝のラッシュでDBが悲鳴を上げます。「統計情報を最新に保つのは、運用側の責任」という意識を持つことが、エンジニアとしてワンランク上に行くコツですよ。
まとめ:迷ったらEXPLAINを見てみよう
結局のところ、`ANALYZE` は「プランナに現実を見せるためのコマンド」です。
もしクエリが遅いと感じたら、まずは `EXPLAIN ANALYZE` で実行計画を確認してください。そこで予測と現実に大きな乖離があれば、それは `ANALYZE` が必要なサインです。
怖がらずに叩いてみてください。適切に統計情報を更新してあげるだけで、クエリは驚くほど軽快に走るようになりますから。
それでは、また現場でお会いしましょう!
コメント