Muhammad Aqeel:Postgres 逆向修復,避免因錯誤更新而浪費整天
Muhammad Aqeel: Undo for Postgres, without losing the rest of your day
pgVolvra 是一套純 SQL 的 Postgres 逆向修復工具,可在不安裝擴充套件、無 superuser 的環境下,精確回復單筆錯誤。
本篇示範如何在 Postgres 上快速回復單筆錯誤,避免長時間手動修復,對日常資料維護工作具有實際可落地的價值。
PostgreSQL 沒有提供單筆語句回復的方式。當有人執行 UPDATE 並遺漏 WHERE 子句時,恢復就變成一個程式,而非單一命令。你需要把資料庫還原到錯誤發生前的狀態,取出受損的資料列,並手動與自那時起已持續變動的實際資料表做對照。
這種恢復方式確實可行,團隊也會採用。但它耗時數小時,需要你最資深的人員,且每一步都可能在表仍在處理流量時讓情況惡化。一次不小心的 SQL 只需一秒,卻可能變成一整天下午的細心工作,且這些人往往是你最難以佔用的資源。
Oracle 使用者在這種情況下有 Flashback 功能,實際上它是兩個功能:一個用來閱讀過去,另一個用來將其恢復。PostgreSQL 既沒有這兩個功能。時間點恢復(point‑in‑time recovery)才是讓該程式可行的關鍵,但它會恢復整個叢集,而非單一語句。你會把它與正式環境一起還原並進行比較,因為直接回溯正式環境會刪除錯誤之後發生的所有變動。我想要更小、更精確的方案,因此我開發了 pgVolvra。
pgVolvra 的功能
pgVolvra 會記錄你選擇的資料表中每一次資料列的變動。對於每一次變更,它都儲存變更前的資料列、變更後的資料列,以及執行此變更的人、時間以及所屬交易。當發生錯誤時,它會利用這些紀錄將受影響的資料列恢復原狀,且不會觸碰其他任何資料。
整個引擎僅使用純 SQL 與 PL/pgSQL。沒有編譯的擴充功能、沒有 shared_preload_libraries、無需 superuser 許可權,也不存取伺服器檔案系統。這是從一開始就設計的限制,也是該工具能在受管理的 PostgreSQL 上執行的原因。
設定方法
安裝方式是將一個檔案執行於你的資料庫。它可以在 psql、任何 SQL 客戶端,或是供應商的網頁控制台中執行。
psql "$DATABASE_URL" -f sql/volvra.sql接著你選擇要監控的資料表。pgVolvra 直到完成此步驟前不會記錄任何變動,也無法恢復此步驟之前發生的任何變更,因而此步驟至關重要。
SELECT volvra.enable('public.orders');這就是整個設定流程。從此點開始,所有對 orders 的 INSERT、UPDATE 與 DELETE 都會被記錄。
實作範例
以下是一個你可以自行建立的資料表的完整流程。我會故意犯同樣的錯誤,然後再逐步將其恢復。
1. 建立一個資料表並啟用 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');2. 故意犯錯。缺少 WHERE 子句,導致所有資料列都被更新,而非單一列。
UPDATE orders SET status = 'cancelled';
UPDATE 33. 檢視某一筆資料列的變化。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"4. 預覽回復。此呼叫不會寫入任何資料。它會構建完整的執行計畫並回傳,讓你在任何變更發生前先閱讀。
SELECT * FROM volvra.preview_undo(
target => 'public.orders',
from_ts => now() - interval '5 min');5. 查詢 pgVolvra。若不傳入 confirm => true,函式會拒絕執行並不改動任何資料,這在測試時已多次為我節省麻煩。
SELECT * FROM volvra.undo(
target => 'public.orders',
from_ts => now() - interval '5 min',
confirm => true);三筆訂單恢復至原始狀態。若客戶在那五分鐘內確實取消了訂單,該取消仍會保留,因為 pgVolvra 只回復已記錄的變更,而非將整張表還原成快照。
時間旅行與早期警示
回復是你在知道錯誤發生後會採取的行動。在此之前,還有兩件事。
第一件事是閱讀過去而不改動任何資料。volvra.as_of 會在任何時間點重建被監控的資料表,且完全不寫入任何資料:
SELECT * FROM volvra.as_of('public.orders', '2026-09-16 15:39');這才是你真正想先做的。決定是否還原任何東西之前,你想先看看錶格當時的樣子,並與現在的狀態做比較。自那時起被刪除的列會重新出現,自那時起新增的列則缺失,而現行表格保持不變。這是 Flashback 的唯讀部分,在你仍在弄清楚發生了什麼時候,執行它是安全的。
第二點是被告知的時間是 15:40,而不是 16:10。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 子句執行時,它會即時大聲說出來,而不是等客戶打電話才讓你發現。
預設情況下警告是關閉的,關閉表示觸發器根本不會被附加,因此不使用它的資料庫不會承擔任何成本。
以你實際描述的方式來說明問題
時間視窗是最粗暴的選項。大多數時候你會知道更具體的錯誤原因,而 pgVolvra 允許你直接說明。
volvra.undo_txid(...) 會在它觸及的每張表上撤銷一次交易。這就是你後悔的遷移。
actor 和 db_user 會撤銷一個應用程式身份或一個資料庫角色的工作。這就是在其他工作正常執行時出錯的匯入工作。
volvra.mark('before-deploy') 為一個時刻命名,而 volvra.undo_to('before-deploy') 則會回到那個時刻。
predicate 將撤銷操作限定為符合條件的列,而 tables 則可在一次計畫中涵蓋多張表。
pgVolvra 為每一次變更記錄兩個身份,而非一個。其一是資料庫角色,另一個是你的連線所設定的應用程式層級 actor。在一個十幾個服務共用同一 Postgres 登入的系統中,僅憑角色本身並不能提供任何有用資訊。
安全規則
一次還原是一次寫入,而不小心的還原會造成比原始錯誤更大的損害。這是我花最多時間的部分。
想像一下在 15:40 做出的錯誤,並在 16:10 才發現。那半小時內,真正的使用者有意改變了同樣的一些列。盲目地把舊值寫回去會在修復你的錯誤時毀掉他們的工作。
因此 pgVolvra 在操作前會檢查每一列。如果某列已不再持有當時的值,表示該列已經變動,整個操作會停止並告訴你哪些列阻礙了程序。你可以傳遞 skip_conflicts 來修復其餘部分,並保留那些列。你無法要求它覆寫它們。沒有強制旗標,我特意不加入,因為強制旗標最終總會被使用,且往往在最糟糕的時刻。
還有一個限制,單次操作可觸及的列數。如果你的過濾條件匹配到遠超預期的列,操作會停止而不是繼續。
歷史資料所在的位置
預設情況下,pgVolvra 將歷史儲存在你自己的資料庫中,存於一個普通表格。這很簡單,無需額外設定,且大部分人都會使用。它也意味著歷史資料與資料庫同命運。若失去資料庫,歷史資料亦會消失。
因此還有第二種選項。一個小型伴隨程序在資料庫外執行,當 Postgres 提交每一次變更時讀取並將其寫入你擁有的儲存,格式為換行分隔的 JSON,並附帶 SHA-256 雜湊清單。這些檔案可在沒有 pgVolvra 與 Postgres 的情況下閱讀。

pgVolvra 透過觸發器在資料庫中記錄變更,或透過伴隨層在外部歸檔中記錄。
兩個層級都不需要對方。它們監控相同的資料表並寫入不同的位置,你可以單獨執行任一層。
啟動伴隨程式只需一條指令。若缺少複寫槽會自動建立,從已存檔案中的最後一段恢復,並持續串流。
volvra-companion run --dsn "$DATABASE_URL" --archive /srv/volvra-archive恢復後的時間段回復
當歷史資料不在資料庫內時,便能實現其他功能,而這正是我最滿意的特性。
有時資料庫本身已遺失,備份才是唯一的回復方式。你的備份時間為 02:00,而現在已是 15:40,恢復後得到的資料庫雖正確,但缺少近十四小時的工作。pgVolvra 能讀取 02:00 之後所歸檔的變更,並再次套用於恢復後的資料庫上。

恢復備份會把資料庫回復到 02:00;重放則回復至 15:40 的已歸檔變更。
恢復會將你帶回過去。重放則再次將已歸檔的歷史往前推進,疊加於恢復後的資料庫之上。
重放使用與撤銷相同的機制,只是方向相反。相同的規劃器、衝突檢查、行數限制、單一交易。我不想為這麼危險的功能再寫一條程式碼路徑。
我也不想僅憑口頭保證釋出此功能,於是測試實際執行。它使用真實的 pg_dump,覆寫更多資料,執行真實的 DROP DATABASE,恢復備份、恢復歸檔、重放,最後對兩邊所有覆蓋表的每一行進行雜湊。回復後的資料庫與丟失的資料庫完全相同,適用於 Postgres 14 至 19。再次重放不會再改變任何內容。
在託管 Postgres 上執行
這是我最關心的部分,因為設計限制在此發揮效果。現在大多數 Postgres 都是租用的,租用版本不允許安裝擴充套件,無法成為 superuser,也無法接觸伺服器檔案系統。
pgVolvra 不需要這些。為了證明相容性,我對每個供應商的實際例項執行了相同的 24 個檢查驗證。
Supabase 與 Neon 在免費方案上皆符合。Supabase 完全無需設定,因為它已為自身的即時功能將 wal_level 設為 logical,且 postgres 角色可建立複寫槽。Neon 只需在控制台啟用一次邏輯複寫,且此變更不可為專案還原,故先驗證撤銷功能,僅在需要歸檔時再開啟。pgEdge Starfleet 一切正常,唯複寫槽建立需由管理員執行一次 ALTER ROLE admin REPLICATION。
Amazon RDS、Aurora 與 Google Cloud SQL 應該亦可在同樣條件下運作,但尚未在那裡執行驗證,因此此處未宣告。
成本
寫入歷史並非免費,以下列出實際數值,而非宣稱開銷極低。
寫入吞吐量下降 11% 至 41%,視語句而定。狹窄的 UPDATE 為最差情形;簡單 INSERT 成本最低,下降 11% 至 12%。
每筆記錄的變更約佔 300 至 400 位元組。
預寫日誌容量約提升 2.2 倍。
不變更任何內容的 UPDATE 會被偵測並跳過,預設僅儲存實際變更的欄位。
這就是為何覆蓋所有表格並非正確做法。只覆蓋那些遺失資料會真正造成損害的表格,其他則保持不變。
pgVolvra 不做的事
它不是備份,也不取代備份。請保留你的備份。
它只知道在你覆蓋表格之後所做的更改。它無法恢復那個時刻之前的任何資料。
重播會恢復行。它不會重播模式變更、序列或你未覆蓋的表格,也不是時間點恢復。
沒有主鍵的表格無法被覆蓋,因為 pgVolvra 會以此方式在稍後再次定位該行。唯一索引不被接受。
關鍵要點
今天修正一條錯誤語句意味著必須還原一份備份並手動比對行,且壓力很大。pgVolvra 只會在一次語句中還原錯誤所觸及的行。
pgVolvra 以一個沒有副檔名且不需要 superuser 的 SQL 檔安裝,因此可以在受管理的 Postgres 上執行。我在 Supabase、Neon 與 pgEdge Starfleet 上進行驗證,而不是假設。
Undo 不會覆寫錯誤發生後已變更的行,而且故意沒有強制覆寫的方式。
有了伴隨的存檔,歷史資料會存活於資料庫之外,重播可以將還原的備份推進到恢復失敗時的幾小時之後。
設定就是整個工作。今天覆蓋一個表格,工具在你需要時才會有用,而不是在此之前。
pgVolvra 1.0.0-beta2 現已釋出。它支援 Postgres 14 至 19,並以 PostgreSQL 許可證開源,程式碼、檔案與測試套件皆在 github.com/pgEdge/pgvolvra。它還是測試版,歡迎在倉庫中真誠報告任何錯誤,亦歡迎提問。