Muhammad Aqeel: PostgresのUndo機能、一日の作業を失うことなく
Muhammad Aqeel: Undo for Postgres, without losing the rest of your day (pgedge.com)
要約
Postgresでは、誤ったSQL文を実行した場合の復旧に時間がかかり、専門知識を要する複雑な手順が必要です。この問題を解決するため、pgVolvraというツールが開発されました。pgVolvraは、テーブルごとの行変更を記録し、誤った変更のみを正確に元に戻す機能を提供します。
全文翻訳
PostgresのUndo機能、一日の作業を失うことなく
Postgresには、単一のステートメントを取り消す方法がありません。WHERE句なしでUPDATEを実行した場合、復旧はコマンドではなく手順となります。間違いが発生した時点のデータベースのコピーを復元し、損傷した行を抽出し、それ以降も動き続けているライブテーブルと手動で照合する必要があります。
この種の復旧は機能しますが、数時間かかり、最も経験豊富な担当者が必要で、テーブルがトラフィックを提供し続けている間に、あらゆるステップで事態を悪化させる可能性があります。悪いSQLの一秒が、最も手が回らない人々による午後の慎重な作業に変わります。
OracleユーザーはまさにこのケースのためにFlashbackを持っており、それは実際には2つの機能です。一つは過去を読み取る機能、もう一つはそれを元に戻す機能です。Postgresにはどちらもありません。ポイントインタイムリカバリは、その手順を可能にしますが、クラスター全体を復元するのであって、ステートメント単位ではありません。本番環境自体を巻き戻すと間違い以降のすべてが破棄されるため、本番環境と並行して復元し比較します。私はもっと小さく、より正確なものを求めていたので、pgVolvraを構築しました。
pgVolvraの機能
pgVolvraは、選択したテーブル上のすべての行変更を記録します。各変更について、変更前の行と変更後の行、そして誰が、いつ、どのトランザクションで変更を加えたかを保持します。問題が発生した場合、その記録を使用して影響を受けた行を元に戻しますが、それ以外のものには一切触れません。
エンジン全体はプレーンSQLとPL/pgSQLでできています。コンパイルされた拡張機能、shared_preload_libraries、スーパーユーザー権限、サーバーのファイルシステムへのアクセスは一切必要ありません。これは初日から設計上の制約であり、ツールがマネージドPostgresで動作する理由です。
セットアップ
インストールは、データベースに対して1つのファイルを実行するだけです。psql、任意のSQLクライアント、またはプロバイダーのWebコンソールから動作します。
psql "$DATABASE_URL" -f sql/volvra.sql
次に、カバーしたいテーブルを選択します。pgVolvraは、これを行うまで何も記録せず、それ以前に発生したことも復元できないため、このステップが重要です。
SELECT volvra.enable('public.orders');
これでセットアップは完了です。この時点から、ordersに対するすべてのINSERT、UPDATE、DELETEが記録されます。
動作例
自分で作成できるテーブルでの完全なシーケンスを以下に示します。意図的に同じ間違いを犯し、それを元に戻します。
- テーブルを作成し、pgVolvraを有効にします。主キーが必要です。これはpgVolvraが後で行を見つける方法だからです。
CREATE TABLE orders (
id bigint PRIMARY KEY,
customer text NOT NULL,
status text NOT NULL,
total numeric(10,2)
);
INSERT INTO orders VALUES
(1, 'ayesha', 'shipped', 240.00),
(2, 'omar', 'pending', 99.50),
(3, 'lena', 'shipped', 18.75);
SELECT volvra.enable('public.orders');
- ミスを犯します。WHERE句がないため、1行ではなくすべてがヒットします。
UPDATE orders SET status = 'cancelled';
UPDATE 3
- 1行に何が起こったかを確認します。
volvra.historyはテーブルと主キーをjsonbとして受け取り、その行のすべてのバージョンを表示します。
SELECT ts, actor, op, old_row -> 'status' AS was,
new_row -> 'status' AS now
FROM volvra.history('public.orders', '{"id": 1}');
ts | actor | op | was | now
---------------------+-------+----+-----------+-------------
2026-09-16 11:04:19 | api | U | "shipped" | "cancelled"
- Undoをプレビューします。この呼び出しは何も書き込みません。正確な計画を構築し、何も変更される前に読み取れるように返します。
SELECT * FROM volvra.preview_undo(
target => 'public.orders',
from_ts => now() - interval '5 min');
- pgVolvraにクエリします。
confirm => trueなしでは、関数は拒否し何も変更しません。これはテスト中に何度も私を救ってくれました。
SELECT * FROM volvra.undo(
target => 'public.orders',
from_ts => now() - interval '5 min',
confirm => true);
3つの注文は元に戻ります。もし顧客が過去5分間に本当に注文をキャンセルしていた場合、そのキャンセルはそのまま残ります。なぜなら、pgVolvraはスナップショットをテーブルに復元するのではなく、記録された変更を逆にたどるからです。
タイムトラベルと早期警告
Undoは、何が間違っていたかを知ったときに手を伸ばすものです。それよりも前の段階に2つのものがあります。
1つ目は、触れることなく過去を読むことです。volvra.as_ofは、任意の時点でのカバードテーブルを再構築し、何も書き込みません。
SELECT * FROM volvra.as_of('public.orders', '2026-09-16 15:39');
これが最初に本当に必要なものです。何かを元に戻すかどうかを決定する前に、テーブルがどのように見えたかを確認し、現在の状態と比較したいはずです。その時点以降に削除された行は戻り、それ以降に作成された行は存在せず、ライブテーブルはそのままです。これはFlashbackの読み取り専用部分であり、何が起こったのかを把握している間は安全に実行できます。
2つ目は、16:10ではなく15:40に知らされることです。pgVolvraは、予想以上に多くの行を変更したステートメントがあった場合に警告できます。
SELECT volvra.set_warn_changed_rows(1000);
UPDATE orders SET status = 'cancelled';
WARNING: volvra: one statement changed more than 1000 row(s) of public.orders
これが機能のすべてです。何もブロックせず、ステートメントを遅くするほどの影響もありません。WHERE句がないSQLが実行された瞬間に、顧客からの電話で発見するのではなく、それを声に出して伝えます。
警告はデフォルトでオフになっており、オフということはトリガーが全くアタッチされていないことを意味します。そのため、それを使用しないデータベースはコストを一切負担しません。
問題を実際に説明する言葉で表現する
時間ウィンドウは、大まかな選択肢です。ほとんどの場合、何が間違っていたかについて、より具体的なことがわかっています。pgVolvraはそれを直接言うことができます。
volvra.undo_txid(...)は、それが触れるすべてのテーブルで1つのトランザクションを逆にします。これは、後悔するマイグレーションです。actorとdb_userは、1つのアプリケーションIDまたは1つのデータベースロールの作業を元に戻します。これは、他のすべてが正常に実行されている間に問題が発生したインポートジョブです。volvra.mark('before-deploy')は、ある時点に名前を付け、volvra.undo_to('before-deploy')はそれに戻ります。predicateは、条件に一致する行にUndoを絞り込み、tablesは複数のテーブルを1つの計画でカバーします。
pgVolvraは、各変更に対して1つではなく2つのIDを記録します。データベースロールと、接続が設定するアプリケーションレベルのアクターがあります。多数のサービスが単一のPostgresログインを共有するシステムでは、ロールだけでは有用な情報は何もわかりません。
安全規則
Undoは書き込みであり、不注意な書き込みは元の間違いよりも大きな損害を与えます。これは私が最も時間をかけた部分です。
15:40に発生した間違いが16:10に気づかれたとします。その30分間に、実際のユーザーが意図的に同じ行の一部を変更しました。古い値を盲目的に書き戻すと、あなたの作業を修復する間に彼らの作業を破壊してしまいます。
そのため、pgVolvraは各行に触れる前にチェックします。行がその時点の値を持っていない場合、その行は移動したと見なされ、操作全体が停止し、邪魔になっている行を通知します。skip_conflictsを渡すことで、残りを修正し、それらの行はそのままにしておくことができます。それらを上書きするように要求することはできません。強制フラグはなく、意図的に削除しました。なぜなら、強制フラグは最終的に常に使用され、常に最悪の日に使用されるからです。
また、1つの操作が触れることができる行数には制限があります。フィルターが予想よりもはるかに多くの行に一致した場合、続行せずに停止します。
ヒストリーはどこに保存されるか
デフォルトでは、pgVolvraはヒストリーを独自のデータベース内に、通常のテーブルとして保持します。これはシンプルで、追加のものは何も必要とせず、ほとんどの人が使用するものです。また、ヒストリーはデータベースと同じ運命を共有することを意味します。データベースを失うと、ヒストリーも失われます。
そのため、2番目のオプションがあります。小さなコンパニオンプロセスがデータベースの外で実行され、Postgresがコミットする各変更を読み取り、所有するストレージに改行区切りJSONとして書き込み、SHA-256ハッシュのマニフェストを付けます。これらのファイルは、pgVolvraやPostgresなしで読み取ることができます。
[画像: Covered tables record row changes through triggers into volvra.change_log and through the write-ahead log into companion JSON archives.]
pgVolvraは、トリガーを通じてデータベースに、またはコンパニオンティアを通じて外部アーカイブに変更を記録します。
どちらのティアも互いを必要としません。同じテーブルを監視し、異なる場所に書き込みます。どちらか一方を単独で実行することもできます。
コンパニオンの起動は1つのコマンドで完了します。レプリケーションスロットが欠落している場合は作成し、アーカイブ済みの最後のセグメントから再開し、継続的にストリーミングします。
volvra-companion run --dsn "$DATABASE_URL" --archive /srv/volvra-archive
リストア後の数時間の回復
ヒストリーがデータベースの外に保存されるようになると、別のことが可能になります。これが私が最も満足している機能です。
データベース自体が失われ、バックアップが唯一の方法となる場合があります。バックアップは02:00のもので、現在は15:40です。リストアは正しいデータベースを提供しますが、ほぼ14時間の作業が失われています。pgVolvraは、02:00以降にアーカイブした変更を読み取り、リストアが提供したものの上に再度適用できます。
[画像: Backup at 02:00, database loss at 15:40, and replay recovering the missing 13 hours and 40 minutes of work.]
バックアップの復元は時間を巻き戻し、リプレイはアーカイブされた変更を15:40まで進めます。
リストアはあなたを過去に戻します。リプレイはアーカイブされたヒストリーを、リストアが提供したものの上に再び進めます。
リプレイは、Undoと同じ仕組みを使用し、逆方向に向けられます。同じプランビルダー、同じ競合チェック、同じ行制限、同じ単一トランザクション。私はこれほど危険なものに2番目のコードパスを必要としませんでした。
また、私の言葉だけで出荷したくなかったので、テストは実際に行います。実際のpg_dumpを取得し、その上にさらにデータを書き込み、実際のDROP DATABASEを実行し、バックアップを復元し、アーカイブを復元し、リプレイを実行し、その後、両側のすべてのカバードテーブルのすべての行のハッシュを計算します。復元されたデータベースは、Postgres 14から19まで、失われたものと同一になります。2回目のリプレイは何も変更しません。
マネージドPostgresでの実行
これが私が最も気にかけている部分です。なぜなら、設計上の制約が報われるからです。今日のPostgresのほとんどはレンタルされており、レンタルPostgresでは拡張機能をインストールできず、スーパーユーザーになれず、サーバーのファイルシステムに近づくこともできません。
pgVolvraはこれらのいずれも必要としません。データシートから互換性を主張するのではなく、各プロバイダーでライブインスタンスに対して同じ24のチェック検証を実行しました。
SupabaseとNeonはどちらも無料プランでパスします。Supabaseは、独自のリアルタイム機能のために既にwal_levelをlogicalとして実行しており、postgresロールがレプリケーションスロットを作成できるため、設定は一切必要ありませんでした。Neonでは、コンソールで論理レプリケーションを一度有効にすると、その変更はプロジェクトで元に戻すことができないため、まずUndo側を確認し、アーカイブが必要な場合にのみオンにしてください。pgEdge Starfleetでは、スロット作成以外はすべて機能しますが、管理者からのALTER ROLE admin REPLICATIONが1つ必要です。
Amazon RDS、Aurora、Google Cloud SQLも同様に機能するはずですが、まだ検証が実行されていないため、ここでは主張していません。
コスト
ヒストリーの書き込みは無料ではないため、オーバーヘッドは最小限であるという主張ではなく、数値を以下に示します。
- ステートメントによっては、書き込みスループットが11%から41%低下します。狭いUPDATEが最悪のケースです。単純なINSERTが最も安価で、11%から12%です。
- 記録された各変更は約300〜400バイトかかります。
- Write-ahead logのボリュームは約2.2倍になります。
- 何も変更しない更新は検出されてスキップされ、デフォルトでは実際に変更された列のみが保存されます。
これが、すべてのテーブルをカバーするのが間違った考え方である理由です。データが失われると本当に困るテーブルだけをカバーし、残りはそのままにしておいてください。
pgVolvraがしないこと
- バックアップではなく、バックアップに取って代わるものでもありません。バックアップは保持してください。
- テーブルをカバーした後に加えられた変更についてのみ知っています。それ以前のことは何も復元できません。
- リプレイは行を復元します。スキーマ変更、シーケンス、またはカバーしなかったテーブルをリプレイするわけではなく、ポイントインタイムリカバリでもありません。
- 主キーのないテーブルはカバーできません。後でpgVolvraが行を見つける方法だからです。一意インデックスは代わりに使用されません。
主要なテイクアウェイ
- 今日の悪いステートメントを修正するには、コピーを復元し、プレッシャーの下で手動で行を照合する必要があります。pgVolvraは、間違いが触れた行のみを、1つのステートメントで元に戻します。
- pgVolvraは、拡張機能やスーパーユーザーなしで1つのSQLファイルとしてインストールされるため、マネージドPostgresで動作します。データシートから想定するのではなく、Supabase、Neon、pgEdge Starfleetで検証しました。
- Undoは、間違いの後に変更された行を上書きすることを拒否し、意図的に強制する方法はありません。
- コンパニオンアーカイブを使用すると、ヒストリーはデータベースよりも長生きし、リプレイは復元されたバックアップを、リストアが失った数時間分進めることができます。
- セットアップがすべての作業です。今日テーブルをカバーすれば、ツールは必要になった日に役立ちます。それより前ではありません。
pgVolvra 1.0.0-beta2がリリースされました。Postgres 14から19をサポートし、PostgreSQLライセンスの下でオープンソースです。コード、ドキュメント、テストスイートはすべてgithub.com/pgEdge/pgvolvraにあります。ベータ版であり、どこで壊れるかの報告や質問は、リポジトリで歓迎します。