Aurora PostgreSQLスナップショット復元と統計情報
目次
Aurora PostgreSQL のスナップショットを検証環境や別アカウントへ復元する運用は、テスト環境のデータ鮮度を保つ手段としてよくある構成です。その設計レビューでよく出る質問が、「復元後に ANALYZE をやり直す必要があるのか」というものです。
この疑問が生まれる背景はだいたい共通しています。復元したクラスターに接続して pg_stat_user_tables を覗くと、last_analyze が空、n_live_tup が 0 になっている。一方で AWS の公式ドキュメントには、メジャーバージョンアップグレードに関して「統計情報は転送されない」という記述がある。この2つが結びついて「スナップショット復元でも統計情報は失われるのでは」という推測になり、復元手順書に「全テーブル ANALYZE」が入る、という流れです。
結論を先に書くと、同一エンジンバージョンでのスナップショット復元では、オプティマイザが使う統計情報は保持されます。pg_stat_user_tables のリセットは別の仕組みの話です。以下、そう言える根拠を PostgreSQL の内部構造から追い、復元直後に確かめるためのクエリ、手順書と監視への落とし込みまで書きます📌
🎯 結論:同一エンジンバージョンの復元なら統計情報は保持される
DB クラスタースナップショットは、ストレージボリューム全体を含む物理的なバックアップです。そして ANALYZE が収集した統計情報は pg_statistic というシステムカタログに格納されており、これは通常のテーブルと同じようにストレージボリューム上に存在します。
つまり、スナップショットを取った時点でボリューム上にあった pg_statistic の行は、そのままスナップショットに含まれ、復元されたクラスターにも存在します。同一エンジンバージョンでの復元であれば、オプティマイザは復元直後から復元前と同じ統計情報を読んで実行計画を立てられます。
逆に言えば、統計情報が失われるのは「ストレージボリュームをそのままコピーするのではない」経路を通ったときです。メジャーバージョンアップグレードや、pg_dump/pg_restore・DMS のような論理移行がそれに当たります。
復元後にANALYZEが必要なケース/不要なケースの早見
判断の分岐は、データの移送方式が物理か論理か、そしてエンジンバージョンが変わるかどうかで決まります。
| 経路 | 統計情報(pg_statistic) | 復元直後の ANALYZE |
|---|---|---|
| 同一エンジンバージョンでのスナップショット復元 | 保持される | 不要 |
| メジャーバージョンアップグレード | 転送されない | 全データベース・全テーブルに対して必要 |
| 論理移行(pg_dump/pg_restore、DMS など) | 引き継がれない | 必要 |
この表の1行目と2行目の違いが、現場で最も混乱しやすいところです。どちらも「Aurora のクラスターが新しくなる」操作に見えるため同じ扱いにしてしまいがちですが、内部でやっていることが根本的に違います。スナップショット復元はボリュームを復元して WAL リカバリをかけるだけで、カタログの中身には手を付けません。メジャーバージョンアップグレードはカタログの構造自体が変わる可能性があるため、統計情報は引き継がれません😇
なお、ここでいう「不要」は「復元直後に全テーブル ANALYZE を流す必要はない」という意味です。復元後に大量のデータ投入やマスキング処理を行うなら、その変更分について ANALYZE を考えます(後述)。
last_analyze が空でも統計が消えたとは限らない
pg_stat_user_tables の n_live_tup、last_analyze、last_autoanalyze といった列は、復元直後にリセットされます。これは仕組みとして予期される挙動で、統計情報が消えたことを意味しません。
この2つは名前が似ているために混同されますが、役割は別物です。
ANALYZE が収集するのは pg_statistic(読みやすく整形したビューが pg_stats)で、列の値の分布に関するデータが入っています。NULL の割合、推定された異なり値数、最頻値リスト、ヒストグラムの境界値など。プランナがコストを見積もるときに読むのはこちらです。対して pg_stat_user_tables が持つのは、テーブルへのアクセス回数、更新された行数、最後に VACUUM/ANALYZE が走った時刻といった累積カウンターで、AutoVacuum / AutoAnalyze が「このテーブルはもう ANALYZE すべきか」を判定するために使います。
last_analyze が空になっていても、それは「このクラスターが起動してから ANALYZE が走った記録がない」という意味にすぎず、pg_statistic の中身とは独立しています。実行計画の品質を気にしているなら、見るべきは pg_stats のほうです。
🧠 なぜ保持されるのか:スナップショットの正体と統計情報の置き場所
結論だけ覚えておいても、バージョンが変わったときや別のケースに遭遇したときに応用が効きません。構造から押さえておきます。
DBクラスタースナップショットはストレージボリュームの物理バックアップ
Aurora の DB クラスタースナップショットは、クラスターのストレージボリューム全体を含む物理的なバックアップです。SQL 文の列やテーブル単位で内容を解釈してコピーしているわけではなく、データが格納されているブロックのレベルで保存されます。
物理バックアップは「テーブルの中身」と「システムカタログの中身」を区別しません。ユーザーが作った orders テーブルのデータブロックも、PostgreSQL が内部で使う pg_class や pg_attribute、そして pg_statistic のデータブロックも、同じストレージボリューム上に並んでいる同格の存在です。
したがって、「スナップショットにユーザーデータは含まれるが統計情報は含まれない」という状態は構造的に起こり得ません。両方が同じボリューム上にあり、ボリュームごと保存されるからです。
オプティマイザが読むのは pg_statistic(pg_stats)というカタログテーブル
PostgreSQL のプランナは、クエリを受け取るとインデックススキャンとシーケンシャルスキャンのどちらが安いか、結合方式は Nested Loop か Hash Join か Merge Join かといった判断をコスト計算で行います。このコスト計算の入力になるのが pg_statistic の内容です。
pg_statistic には、ANALYZE がサンプリングして求めた列ごとの分布情報が入っています。読みやすい形に整形したビューが pg_stats で、実務ではこちらを参照することが多いでしょう。主な列は以下です。
null_frac: その列が NULL である行の割合n_distinct: 異なり値の推定数。正の値なら実数の推定、負の値なら行数に対する比率として解釈されるmost_common_vals/most_common_freqs: 最頻値とその出現頻度。値の偏りが大きい列で、特定の値を指定した検索の推定精度を左右するhistogram_bounds: 最頻値を除いた残りの値の分布を表す境界値の配列。範囲検索の推定に使われるcorrelation: 列の値の順序と物理的な格納順の相関。インデックススキャンのコスト見積もりに影響する
WHERE status = 'paid' のような等値条件の場合、プランナはまず most_common_vals に 'paid' が含まれるかを見ます。含まれていれば対応する most_common_freqs の値から選択率を求め、含まれていなければ n_distinct と最頻値の合計頻度から推定します。この推定が実際の行数から大きく外れると、Nested Loop を選ぶべき場面で Hash Join を選んだり、その逆が起きたりして、実行計画が劣化します😱
ここまで押さえれば、「復元後に実行計画が劣化しないか」という不安の中身が具体化します。確認すべきなのは pg_stats に上記の列が残っているか、そして EXPLAIN の推定行数が妥当かどうかです。
pg_stat_user_tables のカウンターは共有メモリ+統計ファイル由来で復元時にリセットされる
一方で pg_stat_user_tables が参照している累積統計(cumulative statistics)は、まったく別の経路で管理されています。
テーブルへのスキャン回数や更新行数といったカウンターは、各バックエンドプロセスが処理の中でインクリメントし、それが集約されて保持されます。PostgreSQL 15 以降ではこの累積統計を共有メモリ上で管理するようになり、それ以前のバージョンでは統計コレクタープロセスが持ち、定期的にファイルへ書き出す方式でした。いずれの方式でも、最終的な永続化はシャットダウン時のファイル書き出しに依存します。
そしてこの累積統計はクラッシュセーフではありません。正常にシャットダウンされなかった場合、PostgreSQL はこれらのカウンターを破棄してゼロから数え直します。WAL による保護の対象外だからです。
なぜ保護されないかというと、これらのカウンターは「データの正しさ」に関わらないからです。テーブルに何行あるかの推定値(n_live_tup)が 0 にリセットされても、テーブルの実データは1行も失われません。運用上の判定材料が振り出しに戻るだけです。WAL に書いて保護するコストに見合わないため、意図的に非永続として扱われています。
pg_statistic はこれと対照的です。普通のテーブルとしてヒープに格納され、更新は WAL に記録され、クラッシュリカバリで復元されます。統計情報は「データ」、累積カウンターは「実行時のメトリクス」として扱われている、という設計の差です。
クラッシュ整合スナップショットとWALリカバリの関係
Aurora のスナップショットは、稼働中のクラスターからオンラインで取得されます。取得時点でトランザクションが進行中であっても、ストレージボリュームの状態としてはある時点のスナップショットが得られます。復元されたクラスターは起動時に、その時点のデータに WAL(Aurora PostgreSQL の場合は内部的な REDO 処理)を適用してトランザクション的に整合した状態まで持っていきます。
コミット済みのトランザクションは REDO で確実に反映され、未コミットのトランザクションはロールバックされます。これはコミュニティ版 PostgreSQL のクラッシュリカバリと同じ考え方です。
pg_statistic への変更も、通常のテーブル更新と同じく WAL に記録されているため、このリカバリ処理で正しく復元されます。一方、累積統計はリカバリの対象にならず破棄されます。復元直後のクラスターが「正常シャットダウンされなかったインスタンスの再起動」と同じ状態になる、と考えると挙動が整合します。
🔍 復元直後に実行する3つの確認クエリ
復元直後に流す確認クエリを、見る順番とそれぞれで何が分かるかを添えて示します。
1. pg_stats でヒストグラム・MCV・n_distinct が残っているか
まず統計情報の実体を確認します。
SELECT tablename, attname, null_frac, n_distinct, most_common_vals FROM pg_stats WHERE tablename = '<テーブル名>';
行が返り、n_distinct や most_common_vals に値が入っていれば、そのテーブルの統計情報は保持されています。これが確認できれば、少なくともその列についてプランナは復元前と同じ情報を使えます。
💡 確認の対象には、WHERE 句や JOIN 条件でよく使われる列を含むテーブルを選んでください。値の偏りが大きい列(ステータス、フラグ、区分コードなど)は most_common_vals の有無で推定精度が大きく変わるため、優先して見る価値があります。
行がまったく返らない場合は、そのテーブルに対して元々 ANALYZE が実行されていなかった可能性もあります。復元によって消えたのか、元からなかったのかを切り分けるため、可能であればスナップショット取得元のクラスターで同じクエリを実行して比較してください。復元手順の設計段階であれば、取得元での結果を先に記録しておくのが確実です。
2. pg_stat_user_tables でカウンターのリセットを確認する
次に累積カウンターの側を見ます。
SELECT relname, n_live_tup, last_analyze, last_autoanalyze FROM pg_stat_user_tables WHERE relname = '<テーブル名>';
n_live_tup が 0、last_analyze と last_autoanalyze が NULL になっていれば、カウンターがリセットされたことの確認になります。これは異常ではなく、前述の仕組みから予期される結果です。
⚠️ このクエリを「統計情報が消えたかどうかの確認」として使うのは誤りです。ここで見ているのは AutoAnalyze の判定材料であり、実行計画の品質とは直接つながりません。この結果を見て手順書に全テーブル ANALYZE を追加してしまう、という誤りが起きやすい箇所でもあります。
ただし、この結果には運用上の意味があります。カウンターがゼロから数え直しになるため、復元後にデータ変更が発生した場合、AutoAnalyze はそのゼロ起点のカウントに基づいて判定します。この点は後の節で改めて扱います。
3. pg_statistic を直接数えてカタログ側の実体を確かめる
pg_stats はビューなので、権限や条件によって見え方が変わることがあります。カタログテーブル側の実体を直接数えて裏を取ります。
SELECT count(*) FROM pg_statistic s JOIN pg_class c ON c.oid = s.starelid WHERE c.relname = '<テーブル名>';
pg_statistic は列ごと・統計種別ごとに行を持つため、戻る件数はそのテーブルの列数などに依存します。ここで見たいのは「0 ではないこと」です。0 でなければ、カタログ上に統計情報の実体が存在しています。
pg_stats が空なのに pg_statistic の count が 0 でない、という場合は権限や可視性の問題を疑ってください。pg_stats はユーザーが読める行を絞って見せる作りになっています。
EXPLAIN の推定行数で「統計が効いているか」を最終判断する
カタログに統計情報があることと、それがプランナに効いていることは、厳密には別の確認です。最終判断は EXPLAIN の推定行数で行います。
EXPLAIN SELECT * FROM <テーブル名> WHERE <条件列> = '<値>';
見るのは実行計画のノードに出てくる rows= の値です。統計情報がない、あるいは役に立っていない場合、プランナはデフォルトの選択率を使った大雑把な推定に頼ることになり、この値が実データの分布と噛み合わなくなります。
判断基準は、推定行数がその条件で実際に返る行数と同じオーダーに収まっているかどうかです。たとえば値が4種類でほぼ均等に分布している列に等値条件を付けたなら、推定行数は全体の4分の1あたりに来るはずです。そこから桁が違うほど外れていれば、統計情報が使われていないか、統計が古いかを疑います。
⚠️ より厳密に見たい場合は EXPLAIN ANALYZE を使って推定行数と実測行数を並べて比較できますが、実際にクエリが実行されるため、本番相当のデータ量や更新系クエリでは影響範囲に注意してください。
🧪 検証ログ:強制終了→自動リカバリで統計情報はどうなったか
⚠️ 以下はコミュニティ版 PostgreSQL 16 の Docker 環境で実施した検証のログです。Aurora PostgreSQL そのものでの検証ではありません。この前提を踏まえた上で読んでください。
検証環境と、スナップショット復元をどう近似したか
Aurora のスナップショットはクラッシュ整合なバックアップであり、復元されたクラスターは REDO 適用を経て整合した状態で起動します。この「クラッシュ整合な状態からのリカバリ起動」という性質を、コミュニティ版 PostgreSQL では「プロセスの強制終了(停電相当)→ 再起動による自動リカバリ」で近似できます。
検証の流れは以下です。テーブルを作り、値の偏りが分かりやすいデータを投入し、ANALYZE を実行して統計情報を作ります。
CREATE TABLE orders (id bigint PRIMARY KEY, status text, amount int);
INSERT INTO orders SELECT g, (ARRAY['new','paid','shipped','cancelled'])[1 + (g % 4)], (random()*10000)::int FROM generate_series(1,100000) g;
ANALYZE orders;
status 列は4種類の値がほぼ均等に入るため、統計情報が効いていれば等値条件の推定行数が全体の約4分の1になるはずです。これを検証の判定材料にします。
強制終了前後の pg_stats と pg_stat_user_tables の比較
強制終了の前:
attname | null_frac | n_distinct | most_common_vals
---------+-----------+------------+------------------------------
status | 0 | 4 | {paid,shipped,new,cancelled}
relname | n_live_tup | analyzed(last_analyze あり)
---------+------------+----------
orders | 100000 | t
pg_stats には n_distinct = 4、most_common_vals に4つの値が並んでいます。pg_stat_user_tables 側も n_live_tup が投入行数と一致し、last_analyze が記録されています。
強制終了 → 再起動(ログ: database system was not properly shut down; automatic recovery in progress)の後:
attname | null_frac | n_distinct | most_common_vals
---------+-----------+------------+------------------------------
status | 0 | 4 | {paid,shipped,new,cancelled} ← pg_stats は保持
relname | n_live_tup | analyzed
---------+------------+----------
orders | 0 | f ← 統計カウンターはリセット
EXPLAIN SELECT * FROM orders WHERE status = 'paid';
Seq Scan on orders (cost=0.00..1864.00 rows=25273 width=18) ← 保持された統計で推定できている(約25%)
結果は対照的です。pg_stats の null_frac、n_distinct、most_common_vals はいずれも強制終了前と同一で、統計情報は保持されています。一方 pg_stat_user_tables の n_live_tup は 0 になり、last_analyze も失われています。
この2つが同時に起きているのが、冒頭で述べた混乱の正体です。pg_stat_user_tables だけを見ると統計情報が消えたように見えますが、実際にプランナが使う統計情報は残っています😵
EXPLAIN の推定行数から読み取れること
リカバリ後の EXPLAIN では、推定行数が rows=25273 と出ています。テーブル全体が10万行で、status 列は4種類の値がほぼ均等に分布しているため、status = 'paid' に該当するのは約2万5千行です。推定値はこれとよく一致しています。
もし統計情報が失われていたら、プランナは most_common_vals も n_distinct も参照できず、等値条件に対してデフォルトの選択率を当てた推定になります。その場合、推定行数はこの水準には収まりません。推定行数が実際の分布と整合しているという事実が、保持された統計情報がプランナに実際に使われている証拠です。
なお、この例では Seq Scan が選ばれていますが、これはテーブル全体の約25%を読む条件なのでインデックスを使うより安いと判断された結果です。統計情報が効いているからこそ「25%なら全件走査のほうが安い」という妥当な判断ができている、と読むのが正しい理解です。
この検証で言えること・言えないこと
この検証から言えるのは、PostgreSQL の設計として、クラッシュ整合な状態からのリカバリでは pg_statistic の内容が保持され、累積統計カウンターはリセットされる、ということです。前者は WAL で保護されるヒープ上のデータ、後者はクラッシュセーフでない実行時メトリクスという構造の差に由来します。
一方、この検証は Aurora PostgreSQL のスナップショット復元そのものを実施したものではありません。Docker 上のコミュニティ版 PostgreSQL 16 での確認です。Aurora は分散ストレージ層を持つ独自のアーキテクチャであり、スナップショット取得・復元の実装はコミュニティ版のファイルシステムレベルのバックアップとは異なります。
とはいえ、上位のレイヤーで見れば両者は同じ構造を共有しています。pg_statistic が WAL で保護されるヒープ上のカタログテーブルであることも、累積統計がクラッシュセーフでないことも、Aurora PostgreSQL に引き継がれている PostgreSQL の性質です。メモの元になった運用上の確認結果とも一致しています。
💡 自分の環境で確実を期したい場合は、この記事の「復元直後に実行する3つの確認クエリ」を実際の復元フローに組み込み、スナップショット取得元と復元先で結果を比較してください。手順書を書く前に一度やっておけば、以降は根拠を持って判断できます。
🤔 よくある誤解を3つ整理する
誤解1:スナップショット復元後は必ず全テーブルANALYZEが必要
同一エンジンバージョンでのスナップショット復元では、統計情報は保持されるため、復元後に即座に ANALYZE を実行する必要はありません。
この誤解が厄介なのは、実害が見えにくい形で運用コストを増やす点です。大きなテーブルを含むデータベース全体に ANALYZE を流せばそれなりの時間と I/O を消費します。検証環境の準備に不要な待ち時間が入り、復元フローの自動化も重くなります。しかも「念のため」で入っているため、誰も削る判断をしません。
一方で、復元後に「データ変更を伴う処理」を行う場合は話が別です。個人情報のマスキング、テストデータの追加投入、不要データの削除などを行えば、その処理でデータの分布が変わり、保持されていた統計情報は実データと乖離します。この場合に ANALYZE が必要なのは「復元したから」ではなく「データを変更したから」です。理由を正しく切り分けておくと、手順書の記述も自然に整理されます。
誤解2:「メジャーバージョンアップグレードで統計情報は転送されない」は復元にも当てはまる
メジャーバージョンアップグレード時に統計情報が転送されないという記述は、スナップショット復元とは異なる文脈の話です。
この2つを混同してしまう理由は理解できます。どちらも「新しい Aurora クラスターが出来上がる」操作に見えるからです。しかし内部でやっていることが違います。
同一バージョンのスナップショット復元は、ストレージボリュームを復元して REDO を適用するだけです。カタログの構造もバージョンも変わらないため、pg_statistic の行はそのまま有効に使えます。
メジャーバージョンアップグレードでは、統計情報の格納形式や統計の種類自体がバージョン間で変わる可能性があります。異なるバージョン間で統計情報をそのまま引き継ぐと、プランナが不正な値を読む危険があるため、引き継がず ANALYZE による再生成を求める設計になっています。
💡 手順書上では、「同一バージョン復元」と「メジャーバージョンアップグレード」を明確に別の分岐として書き分けます。同じ節にまとめて書くと、必ずどちらかの記述が他方に引きずられます。
誤解3:last_analyze が空=統計情報が存在しない
last_analyze は「このインスタンスが起動してから ANALYZE が実行された記録」であり、統計情報そのものの有無ではありません。
この誤解はおそらく最も頻繁に起きます。復元直後の確認で pg_stat_user_tables を見るのは自然な動作で、そこで last_analyze が NULL なのを見れば「ANALYZE されていない」と読むのは無理のない推論です。
ただ、この列が答えられるのは「AutoAnalyze の判定材料としての最終実行時刻」までです。実行計画の品質を知りたいなら pg_stats と EXPLAIN を見てください。確認クエリの順番を「まず pg_stats、次に pg_stat_user_tables」としているのは、この誤読を避けるためでもあります。
同じ話が n_live_tup にも当てはまります。これは AutoVacuum / AutoAnalyze の判定に使う推定行数であり、テーブルの実際の行数ではありません。0 になっていてもデータは1行も失われていません。実際の行数を知りたいなら SELECT count(*) を使ってください。
🛠️ 復元後にやるべきこと・やらなくていいこと
メジャーバージョンアップグレード後の全DB・全テーブルANALYZE
メジャーバージョンアップグレードを実施した場合は、全データベース・全テーブルに対して ANALYZE を実行して統計情報を再生成します。
⚠️ ここで見落としやすいのが「全データベース」の部分です。1つのクラスター内に複数のデータベースがある構成では、接続先のデータベースだけ ANALYZE して終わりにしてしまうことがあります。ANALYZE は接続しているデータベース内が対象なので、データベースごとに接続して実行します。
💡 また、アップグレード直後のインスタンスは統計情報がない状態で稼働を始めます。この間はプランナがデフォルトの選択率で推定するため、実行計画が意図しないものになる可能性があります。本番のアップグレードでは、ANALYZE の完了をアプリケーション接続開放の前提条件として手順に組み込んでおくのが安全です。
論理移行(pg_dump/pg_restore・DMS)では統計は引き継がれない
pg_dump / pg_restore や DMS のような論理移行では、統計情報は引き継がれません。
理由は構造から明らかです。pg_dump が出力するのはスキーマ定義とデータの SQL 文(または独自形式のアーカイブ)で、pg_statistic の中身はその対象に含まれません。DMS はソースから読んだデータをターゲットに書き込む仕組みなので、そもそもカタログの統計情報を運ぶ経路がありません。
論理移行では、ターゲット側でデータを投入し終わった時点で ANALYZE を実行します。大量のデータをロードした直後は、統計情報がないだけでなく Visibility Map も未整備の状態なので、VACUUM ANALYZE をかけるかどうかも移行後の運用方針に含めて検討してください。
移行方式の選定段階で「物理か論理か」を意識しておくと、この後工程の有無を見積もりに入れられます。復元手順の設計でこの観点が抜けると、移行後の性能確認で時間を取られます。
復元直後のAutoAnalyze判定がどう動くかを踏まえた初回ANALYZEの考え方
スナップショット復元後は AutoVacuum / AutoAnalyze のカウンターがリセットされます。この影響を理解しておくと、初回 ANALYZE についての判断が変わります。
AutoAnalyze は、テーブルに対する変更行数の累積が一定の基準を超えたときに ANALYZE を起動します。この判定に使われるのが、リセットされた pg_stat_user_tables のカウンターです。つまり復元後は、ゼロ起点のカウントに基づいて改めて判定されます。
ここから導かれる運用上の含意は2つあります。
1つは、復元前に「もうすぐ AutoAnalyze が走るはずだった」テーブルがあっても、その進捗はリセットされるということです。復元後はゼロから数え直しになるため、そのテーブルの AutoAnalyze はより後になります。保持されている統計情報がすでに古くなっているテーブルでは、これが実行計画の劣化として現れる余地があります。
⚠️ もう1つは、n_live_tup が 0 になっていることが判定に影響する点です。AutoAnalyze の起動判定の閾値は推定行数を使って計算される仕組みなので、その推定行数が 0 の状態では、少量の変更でも判定が成立する可能性があります。閾値の具体的な計算式やパラメータのデフォルト値は PostgreSQL のバージョンや Aurora のパラメータグループ設定によって変わりうるため、自環境の値は公式ドキュメントと実際のパラメータグループで確認してください。
実務的な落としどころとしては、次のように考えると整理しやすくなります。
- 復元してそのまま参照用途に使う場合: 復元直後の ANALYZE は不要
- 復元後にマスキングや大量のデータ投入・削除を行う場合: その処理の完了後に、変更したテーブルに対して ANALYZE を実行する
- 復元前から統計情報が古かったことが分かっている場合: 復元とは独立した理由として、該当テーブルに ANALYZE を実行する
判断軸を「復元したから ANALYZE」から「データを変えたから ANALYZE」「統計が古いから ANALYZE」に置き換えると、手順書の記述もぶれなくなります。
パラメータグループやインスタンスクラスの差が実行計画に与える影響
統計情報が保持されていても、復元先で実行計画が復元元と変わることはあります。統計情報以外の要素が計画に影響するためです。
プランナのコスト計算には、統計情報のほかにコストパラメータが使われます。random_page_cost、seq_page_cost、effective_cache_size、work_mem などです。復元元と復元先で異なるパラメータグループを使っていれば、同じ統計情報から同じ計画が出るとは限りません。
インスタンスクラスの違いも効いてきます。Aurora PostgreSQL では shared_buffers などのメモリ関連パラメータがインスタンスのメモリサイズに応じた式で設定されるのが一般的です。本番が大きめのクラス、検証環境が小さめのクラスという構成だと、work_mem に依存する Hash Join や Sort の選択が変わる可能性があります。検証環境で Sort がディスクにあふれて遅い、というのはこのパターンです。
このため、「復元後に実行計画が違う」という現象に遭遇したときの切り分け順序は次のようになります。
pg_statsを復元元と復元先で比較 → 統計情報の差か- 両環境の
SHOWでコストパラメータとメモリ関連パラメータを比較 → パラメータグループの差か - インスタンスクラスとそれに紐づくメモリ設定を比較 → リソースの差か
統計情報を疑って ANALYZE を流しても計画が変わらない場合、原因は2か3にあります。この切り分け順を持っておくと、無駄な ANALYZE を避けられます。
❓ よくある質問
Aurora PostgreSQL のスナップショットを復元したら ANALYZE をやり直す必要がある?
同一エンジンバージョンでのスナップショット復元であれば、オプティマイザが使う統計情報は保持されるため、復元直後に全テーブル ANALYZE を流す必要はありません。DB クラスタースナップショットはストレージボリューム全体の物理バックアップで、統計情報を格納する pg_statistic も同じボリューム上にあるためです。ただし復元後にマスキングや大量のデータ投入・削除を行う場合は、データの分布が変わるので変更したテーブルに ANALYZE を実行します。判断軸は「復元したから」ではなく「データを変えたから」「統計が古いから」です。
復元直後に last_analyze が NULL で n_live_tup が 0 なのは統計情報が消えたから?
いいえ。pg_stat_user_tables の n_live_tup・last_analyze・last_autoanalyze は累積カウンターで、クラッシュセーフではないため復元時にリセットされます。これは仕組みから予期される挙動で、プランナが読む pg_statistic(pg_stats)の中身とは独立しています。実行計画の品質を確認したいなら pg_stats と EXPLAIN を見てください。
復元後に統計情報が効いているかどうかはどう確認できる?
まず pg_stats で null_frac・n_distinct・most_common_vals が残っているかを見て、次に pg_stat_user_tables でカウンターのリセットを確認し、pg_statistic の count で 0 でないことを裏取りします。最終判断は EXPLAIN の rows= が、その条件で実際に返る行数と同じオーダーに収まっているかで行います。桁が違うほど外れていれば、統計情報が使われていないか古いかを疑います。可能ならスナップショット取得元でも同じクエリを実行して比較すると確実です。
統計情報が保持されていても復元先で実行計画が変わることはある?
あります。プランナのコスト計算には統計情報以外に random_page_cost、seq_page_cost、effective_cache_size、work_mem などのコストパラメータが使われるため、パラメータグループが違えば同じ計画になるとは限りません。インスタンスクラスの違いによるメモリ関連パラメータの差で、Hash Join や Sort の選択が変わることもあります。切り分けは pg_stats の比較 → SHOW でのパラメータ比較 → インスタンスクラスとメモリ設定の比較の順で行い、無駄な ANALYZE を避けます。
📋 運用手順書と監視への落とし込み
復元手順書に書き分けるべき分岐(同一バージョン復元/メジャーVUP/論理移行)
手順書では、統計情報の扱いを経路ごとに別の項として書き分けてください。まとめて書くと必ず混線します。
同一エンジンバージョンでのスナップショット復元の項には、統計情報が保持されるためこの時点での ANALYZE は不要であること、pg_stat_user_tables のカウンターはリセットされるが異常ではないこと、そして確認手段として pg_stats を見ることを書きます。復元後にデータ変更を伴う処理(マスキングなど)を行う場合は、その処理の後工程として ANALYZE を置く、という条件分岐も明記します。
メジャーバージョンアップグレードの項には、統計情報が転送されないため全データベース・全テーブルの ANALYZE が必要であること、複数データベースがある場合はデータベースごとに実行すること、アプリケーション接続の開放は ANALYZE 完了後とすることを書きます。
論理移行(pg_dump/pg_restore・DMS)の項には、統計情報が引き継がれないためデータ投入後の ANALYZE が必要であることを書きます。
この3つを別項にしておけば、「メジャーVUP で必要だから復元でも必要なのでは」という推測が入り込む余地がなくなります。手順書のレビューで指摘を受けたときも、根拠を示して説明できます。
復元直後に取っておくと後で助かる情報
💡 復元後に「性能が出ない」「実行計画が違う」という問い合わせが来たときに、後から比較材料を集めるのは大変です。復元フローの中で以下を取得して保存しておくと、切り分けが格段に楽になります。
- 主要テーブルの
pg_stats(null_frac、n_distinct、most_common_vals) - 主要テーブルの
pg_stat_user_tables(リセット状態の記録として) - 主要テーブルに対する
pg_statisticの count - 業務上重要なクエリの
EXPLAIN出力 - コストパラメータとメモリ関連パラメータの
SHOW結果
可能なら、スナップショット取得元のクラスターでも同じ内容を取得して並べて保存してください。「復元前後で pg_stats が同一である」という記録があれば、統計情報の疑いを最初の段階で外せます。逆に差異があれば、そこが調査の起点になります。
これらの取得はすべて読み取り専用のクエリで済むため、復元フローのスクリプトに組み込むコストは低いはずです。
実行計画の劣化に早く気づくための監視ポイント
復元環境に限らず、実行計画の劣化に早く気づくための観点をまとめます。
最も直接的な指標は、推定行数と実測行数の乖離です。auto_explain を有効にして実行計画をログに出す構成にしておくと、遅いクエリの計画を後から確認できます。auto_explain.log_analyze を有効にすれば推定と実測の両方が記録されますが、クエリ実行にオーバーヘッドが乗るため、有効化の範囲と本番影響は事前に評価してください。
AutoAnalyze の実行状況も見ます。pg_stat_user_tables の last_autoanalyze と n_mod_since_analyze を定期的に取得しておくと、更新が多いのに ANALYZE が走っていないテーブルを見つけられます。復元後はカウンターがゼロ起点になっているため、この値の推移を追う意味が特にあります。
待機イベントの変化も手がかりになります。実行計画が劣化すると、待機イベントの構成が変わります。想定外のシーケンシャルスキャンが増えれば I/O 系の待機が増え、Hash Join が work_mem を超えて一時ファイルを使うようになれば一時ファイル関連の I/O が現れます。Performance Insights で待機イベントの傾向を見ておくと、SQL 単位で追う前に当たりをつけられます。
一時ファイルの発生も拾っておきます。log_temp_files を設定して一時ファイルの発生をログに出しておくと、work_mem 不足によるディスクソート・ディスクハッシュを検知できます。検証環境が本番より小さいインスタンスクラスの場合、ここが最初に現れる差異になりやすいポイントです。
いずれも「復元したから入れる監視」ではなく通常運用で持っていたい観点ですが、復元環境を新設するタイミングは設定を見直す良い機会です。
OSS-DB Silver 対策テキスト(OSS教科書)
PostgreSQLの仕組みを体系的に学ぶなら、OSS-DB Silverの公式対応テキストが近道です。資格を取らなくても基礎固めに役立ちます。
スナップショット復元と統計情報の関係は、「物理バックアップは pg_statistic を含むボリュームごと保存する」「累積統計カウンターはクラッシュセーフでないためリセットされる」という2つの構造を押さえれば、迷わず判断できます。この2つを混同したまま手順書を書くと、不要な ANALYZE が恒久的に残ったり、必要な ANALYZE が抜け落ちたりします🧭
なお、Aurora PostgreSQL の仕様やスナップショット・アップグレードの挙動、パラメータのデフォルト値は変更されることがあります。実際の手順を確定させる前に、AWS および PostgreSQL の公式ドキュメントで対象バージョンの記述を確認してください。