Undo voor Postgres, zonder de rest van je dag te verliezen
Muhammad Aqeel | 22 oktober 2026
Postgres biedt geen manier om een enkele statement ongedaan te maken. Wanneer iemand een UPDATE uitvoert zonder WHERE-clausule, is herstel een procedure in plaats van een commando. Je moet een kopie van de database herstellen naar het moment vlak voor de fout, de beschadigde rijen eruit halen en deze handmatig reconciliëren met een live tabel die in de tussentijd is blijven veranderen.
Dit soort herstel werkt, en teams doen het. Het kost echter uren, vereist je meest senior medewerker, en elke stap is een kans om zaken te verergeren terwijl de tabel nog steeds verkeer verwerkt. Eén seconde van slechte SQL verandert in een middag zorgvuldig werk door de mensen die je het minst kunt missen.
Oracle-gebruikers hebben Flashback voor precies dit scenario, en dat bestaat eigenlijk uit twee functies: één om het verleden te lezen en één om het terug te zetten. Postgres heeft beide niet. Point-in-time recovery is wat die procedure mogelijk maakt, maar het herstelt een volledig cluster in plaats van een enkele statement. Je herstelt het naast de productieomgeving en vergelijkt ze, omdat het terugdraaien van de productieomgeving zelf alles zou wissen wat er na de fout is gebeurd. Ik wilde iets kleiners en precizers, dus ik heb pgVolvra gebouwd.
Wat pgVolvra doet
pgVolvra registreert elke wijziging in de rijen van de tabellen die jij kiest. Voor elke wijziging bewaart het de rij zoals deze was en de rij zoals deze werd, samen met wie de wijziging heeft gemaakt, wanneer en in welke transactie. Wanneer er iets misgaat, gebruikt het dat record om de getroffen rijen terug te zetten, en raakt het niets anders aan.
De gehele engine bestaat uit gewone SQL en PL/pgSQL. Er is geen gecompileerde extensie, geen sharedpreloadlibraries, geen superuser en geen toegang tot het bestandssysteem van de server. Dat was vanaf de eerste dag een ontwerpeis, en het is de reden waarom de tool überhaupt op managed Postgres draait.
Installatie en configuratie
De installatie bestaat uit het uitvoeren van één bestand tegen je database. Het werkt vanuit psql, vanuit elke SQL-client of vanuit de webconsole van een provider:
psql "$DATABASE_URL" -f sql/volvra.sql
Vervolgens kies je de tabellen die je wilt beschermen. pgVolvra registreert niets totdat je dit doet, en het kan niets herstellen wat vóór dat moment is gebeurd; deze stap is dus essentieel.
SELECT volvra.enable('public.orders');
Dat is de volledige setup. Vanaf dit moment wordt elke INSERT, UPDATE en DELETE op de tabel orders geregistreerd.
Een werkend voorbeeld
Hier is de volledige sequentie op een tabel die je zelf kunt maken. Ik maak expres dezelfde fout en loop daar vervolgens weer uit.
- Maak een tabel aan en activeer pgVolvra. Een primary key is vereist, omdat pgVolvra dit gebruikt om een rij later weer terug te vinden.
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');
- Maak de fout. De
WHERE-clausule ontbreekt, waardoor elke rij wordt geraakt in plaats van één.
UPDATE orders SET status = 'cancelled';
-- Resultaat: UPDATE 3
- Bekijk wat er met één rij is gebeurd.
volvra.historyneemt de tabel en de primary key alsjsonben toont elke versie van die rij.
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" |
- Voorbeeld van de undo. Deze aanroep schrijft niets. Het bouwt het exacte plan op en geeft dit terug, zodat je het kunt lezen voordat er iets verandert.
SELECT * FROM volvra.preview_undo(
target => 'public.orders',
from_ts => now() - interval '5 min'
);
- Query pgVolvra. Zonder
confirm => trueweigert de functie en verandert hij niets, wat mij tijdens het testen al vaker heeft gered.
SELECT * FROM volvra.undo(
target => 'public.orders',
from_ts => now() - interval '5 min',
confirm => true
);
De drie bestellingen gaan terug naar hoe ze waren. Als een klant tijdens die vijf minuten echt een bestelling had geannuleerd, zou die annulering blijven staan, omdat pgVolvra geregistreerde wijzigingen omkeert in plaats van een snapshot over de tabel heen te herstellen.
Tijdreizen en vroegtijdige waarschuwingen
Undo is waar je naar grijpt zodra je weet wat er mis is gegaan. Er zijn twee functies die daarvóór komen.
Ten eerste: het lezen van het verleden zonder het aan te raken. volvra.as_of herbouwt een beschermde tabel zoals deze op elk gewenst moment was, zonder iets te schrijven:
SELECT * FROM volvra.as_of('public.orders', '2026-09-16 15:39');
Dit is wat je eigenlijk eerst wilt doen. Voordat je besluit iets ongedaan te maken, wil je zien hoe de tabel eruitzag en dit vergelijken met de huidige staat. Rijen die sinds dat moment zijn verwijderd komen terug, rijen die daarna zijn aangemaakt zijn afwezig, en de live tabel blijft onaangetast. Dit is het read-only gedeelte van Flashback, en het is veilig om uit te voeren terwijl je nog uitzoekt wat er is gebeurd.
Ten tweede: worden gewaarschuwd om 15:40 in plaats van om 16:10. pgVolvra kan waarschuwen wanneer één statement meer rijen wijzigt dan je verwacht:
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
Dat is de hele functie. Het blokkeert niets en vertraagt de statement niet merkbaar. Het zegt simpelweg iets hardop op het moment dat een ontbrekende WHERE-clausule wordt uitgevoerd, in plaats van dat je er pas achter komt wanneer een klant belt.
De waarschuwing staat standaard uit. "Uit" betekent dat de triggers helemaal niet zijn gekoppeld, dus een database die dit niet gebruikt, draagt geen kosten met zich mee.
Het probleem specifiek omschrijven
Een tijdvenster is de botte optie. De meeste keren weet je iets specifiekers over wat er mis is gegaan, en pgVolvra stelt je in staat dit direct aan te geven:
volvra.undo_txid(...)keert één transactie om over elke tabel die deze raakt. Dit is de migratie waar je spijt van hebt.actorendb_userkeren het werk van één applicatie-identiteit of één databaserol om. Dit is de importjob die misging terwijl alles andere normaal bleef draaien.volvra.mark('before-deploy')benoemt een moment, envolvra.undo_to('before-deploy')keert terug naar dat moment.predicatebeperkt een undo tot rijen die aan een voorwaarde voldoen, entablesbeslaat meerdere tabellen in één plan.
pgVolvra registreert twee identiteiten voor elke wijziging, niet één. Er is de databaserol, en er is een actor op applicatieniveau die je verbinding instelt. In een systeem waar een dozijn services één enkele Postgres-login delen, vertelt de rol op zichzelf niets nuttigs.
Veiligheidsregels
Een undo is een schrijfactie, en een onvoorzichtige undo kan meer schade aanrichten dan de oorspronkelijke fout. Dit is het onderdeel waar ik de meeste tijd aan heb besteed.
Denk aan een fout gemaakt om 15:40 die om 16:10 wordt opgemerkt. In dat half uur hebben echte gebruikers opzettelijk sommige van dezelfde rijen gewijzigd. Het blindelings terugschrijven van de oude waarden zou hun werk vernietigen terwijl jouw fout wordt hersteld.
Daarom controleert pgVolvra elke rij voordat deze wordt aangeraakt. Als een rij niet langer de waarde heeft die hij op dat moment had, is die rij verder geëvolueerd; de hele operatie stopt dan en meldt welke rijen in de weg staan. Je kunt skip_conflicts meegeven om de rest te herstellen en die rijen ongemoeid te laten. Je kunt het niet vragen om ze te overschrijven. Er is geen force-vlag, en ik heb die bewust weggelaten, omdat een force-vlag uiteindelijk altijd wordt gebruikt, en altijd op de slechtste dag.
Er is ook een limiet aan hoeveel rijen één operatie mag raken. Als je filter veel meer rijen raakt dan je verwachtte, stopt het systeem in plaats van door te gaan.
Waar de geschiedenis wordt opgeslagen
Standaard bewaart pgVolvra de geschiedenis in je eigen database, in een gewone tabel. Dat is eenvoudig, vereist niets extra's en is wat de meeste mensen gebruiken. Het betekent echter ook dat de geschiedenis hetzelfde lot deelt als de database. Verlies je de database, dan is ook de geschiedenis weg.
Er is daarom een tweede optie. Een klein begeleidend proces (companion process) draait buiten de database, leest elke wijziging zodra Postgres deze commit en schrijft deze naar opslag die jij beheert als newline delimited JSON met een manifest van SHA-256 hashes. Deze bestanden kunnen worden gelezen zonder pgVolvra en zonder Postgres.
pgVolvra registreert wijzigingen in de database via triggers, of in een extern archief via de companion tier. Beide tiers hebben elkaar niet nodig; ze bewaken dezelfde tabellen maar schrijven naar verschillende plaatsen, en je kunt beide onafhankelijk van elkaar draaien.
Het starten van de companion vereist één commando. Het maakt de replicatieslot aan als deze ontbreekt, hervat vanaf het laatste segment dat al in het archief staat en streamt continu:
volvra-companion run --dsn "$DATABASE_URL" --archive /srv/volvra-archive
Het herstellen van de uren na een restore
Zodra de geschiedenis buiten de database leeft, wordt iets anders mogelijk, en dit is de functie waar ik het meest trots op ben.
Soms is de database zelf weg en is een backup de enige weg terug. Je backup is van 02:00, het is nu 15:40, en de restore geeft je een correcte database die bijna veertien uur aan werk mist. pgVolvra kan de wijzigingen lezen die na 02:00 zijn gearchiveerd en deze opnieuw toepassen bovenop wat de restore heeft opgeleverd.
Het herstellen van de backup brengt de database terug naar 02:00; de replay herstelt de gearchiveerde wijzigingen tot 15:40. De restore verplaatst je terug in de tijd. Replay verplaatst de gearchiveerde geschiedenis weer vooruit, bovenop wat de restore heeft opgeleverd.
Replay gebruikt dezelfde mechanismen als undo, maar dan in de andere richting. Dezelfde plan-builder, dezelfde conflictcontroles, dezelfde rijlimiet, dezelfde enkele transactie. Ik wilde geen tweede codepad voor iets dat zo gevaarlijk is.
Ik wilde dit ook niet alleen op basis van mijn eigen woord verspreiden, dus voert de test het echt uit. Het neemt een echte pg_dump, schrijft er meer data bovenop, voert een echte DROP DATABASE uit, herstelt de backup, herstelt het archief, voert de replay uit en hasht vervolgens elke rij van elke beschermde tabel aan beide kanten. De herstelde database komt identiek uit aan degene die verloren was, op Postgres 14 tot en met 19. Een tweede replay verandert niets meer.
Gebruik op Managed Postgres
Dit is het deel dat ik het belangrijkste vind, omdat hier de ontwerpeis zijn vruchten afwerpt. De meeste Postgres-installaties tegenwoordig zijn gehuurd, en gehuurde Postgres laat je geen extensie installeren, maakt je geen superuser en laat je niet in de buurt van het bestandssysteem van de server.
pgVolvra heeft niets daarvan nodig. In plaats van compatibiliteit te claimen op basis van een datasheet, heb ik dezelfde 24-punts verificatie uitgevoerd tegen live instances bij elke provider.
- Supabase en Neon both pass op hun gratis plannen. Supabase vereiste geen configuratie, omdat het
wal_levelal oplogicalheeft staan voor zijn eigen realtime-functie en depostgres-rol een replicatieslot kan maken. Bij Neon activeer je logische replicatie eenmaal in de console; deze wijziging kan niet ongedaan worden gemaakt voor het project, dus verifieer eerst de undo-zijde en zet het pas aan als je het archief wilt. - Op pgEdge Starfleet werkt alles behalve de slotcreatie, wat een
ALTER ROLE admin REPLICATIONvan een beheerder vereist. - Amazon RDS, Aurora en Google Cloud SQL zouden op dezelfde basis moeten werken, maar de verificatie is daar nog niet uitgevoerd, dus worden ze hier niet geclaimd.
De kosten (overhead)
Het schrijven van geschiedenis is niet gratis, dus hier zijn de cijfers in plaats van een claim dat de overhead minimaal is.
- De schrijfsnelheid (write throughput) daalt met 11 tot 41 procent, afhankelijk van de statement. Een smalle
UPDATEis het slechtste scenario. Een eenvoudigeINSERTis het goedkoopst met 11 tot 12 procent. - Elke geregistreerde wijziging kost ongeveer 300 tot 400 bytes.
- Het volume van de Write-ahead log (WAL) stijgt met ongeveer 2,2 keer.
- Updates die niets veranderen worden gedetecteerd en overgeslagen, en standaard worden alleen de kolommen opgeslagen die daadwerkelijk zijn gewijzigd.
Dit is waarom het beschermen van elke tabel het verkeerde instinct is. Bescherm alleen de tabellen waar dataverlies echt pijn zou doen en laat de rest ongemoeid.
Wat pgVolvra niet doet
- Het is geen backup en vervangt deze niet. Blijf je backups maken.
- Het weet alleen van wijzigingen die zijn gemaakt nadat je een tabel hebt beschermd. Het kan niets herstellen van vóór dat moment.
- Replay herstelt rijen. Het herstelt geen schemawijzigingen, sequenties of tabellen die je nooit hebt beschermd, en het is geen point-in-time recovery.
- Een tabel zonder primary key kan niet worden beschermd, omdat pgVolvra dit nodig heeft om de rij later weer terug te vinden. Een unieke index wordt niet geaccepteerd als vervanging.
Belangrijkste conclusies
- Het herstellen van één slechte statement betekent vandaag de dag het herstellen van een kopie en het handmatig reconciliëren van rijen onder hoge druk. pgVolvra keert alleen de rijen om die de fout raakte, in één statement.
- pgVolvra wordt geïnstalleerd als één SQL-bestand zonder extensie en zonder superuser, waardoor het op managed Postgres draait. Dit is geverifieerd op Supabase, Neon en pgEdge Starfleet.
- Undo weigert rijen te overschrijven die zijn gewijzigd na de fout, en er is bewust geen manier om dit te forceren.
- Met het companion-archief overleeft de geschiedenis de database, en replay kan een herstelde backup vooruitbrengen over de uren die de restore verloor.
- De setup is het hele werk. Bescherm een tabel vandaag, en de tool is nuttig op de dag dat je hem nodig hebt, niet ervoor.
pgVolvra 1.0.0-beta2 is nu beschikbaar. Het ondersteunt Postgres 14 tot en met 19, is open source onder de PostgreSQL License, en de code, documentatie en testsuite zijn te vinden op github.com/pgEdge/pgvolvra. Het is een beta, en meldingen over waar het stukloopt zijn zeer welkom, evenals vragen in de repository.
Groetjes,