Databases
Postgres 12-audit: de checklist vóór een AI-retrofit
Vrijdagmiddag, een read-replica toegekend, deadline op maandag. Dit is de saaie audit-checklist die we draaien voor we AI-agent werk offreren op Postgres 12 bij een Nederlandse mkb'er.

Een vrijdagmiddag in Utrecht. De CTO van een groothandel met 38 medewerkers heeft ons net via een tijdelijke IP-allowlist toegang gegeven tot een read-replica. Hij wil voor maandag weten of we een klantenservice-agent op de orderdatabase kunnen aansluiten zonder de nachtelijke batch te slopen. Wij offreren pas als we onderstaande checklist hebben gedraaid. Op een gezond cluster kost dat ongeveer een uur, op een ziek cluster bijna een hele dag, en het antwoord op de vraag kunnen we deze agent leveren ligt ergens in die uitkomst begraven.
De meeste Nederlandse mkb-clusters die we in 2026 auditen draaien nog op PostgreSQL 12. Community-support is op 14 november 2024 gestopt, maar de database doet z'n werk, de hostingrekening is voorspelbaar, en niemand had reden om te migreren. Op het moment dat iemand er een AI-agent op wil zetten, klopt die rekening niet meer. De agent gaat meer lezen, meer schrijven, en hecht veel waarde aan consistente snapshots waar het oorspronkelijke schema nooit op getuned is.
Waarom we auditen voor we offreren
We hebben twee keer een retrofit geoffreerd zonder audit. Beide keren betaalden we het verschil zelf. Hetzelfde patroon: een query die de agent veertig keer per minuut afvuurde, stond niet in pg_stat_statements, dus we misten 'm tijdens scoping. Tegen week drie liep de replica-lag op tijdens kantooruren en zaten we om 22:00 op een dinsdag te debuggen. Gratis les, betaald in onze uren.
De checklist hieronder is wat we voor de laatste veertien mkb-prospects tussen januari en mei hebben gedraaid. Bewust saai. Er zit geen LLM in. Hij beantwoordt alleen drie vragen: houdt de autovacuum bij op de tabellen waaruit we gaan lezen, hebben we zicht op wat er nu al draait, en kunnen we dit cluster naar een ondersteunde major-versie tillen zonder ook maar één rij te verliezen die de AVG als audit-log-entry beschouwt.
Autovacuum-lag scoren op tabellen boven de 40 GB
Veertig gigabyte is de drempel waarboven autovacuum-gedrag geen default-settings-verhaal meer is, maar een per-tabel-verhaal. Daaronder doet de standaard autovacuum_vacuum_scale_factor van 0.2 het meestal prima. Daarboven betekent een scale factor van 0.2 dat autovacuum wacht tot er 8 GB aan dode tuples zijn opgehoopt voor hij überhaupt draait, en tegen die tijd vecht de cleanup met kantooruren om I/O.
We halen het beeld op met deze query:
SELECT
schemaname || '.' || relname AS table,
pg_size_pretty(pg_total_relation_size(relid)) AS size,
n_live_tup,
n_dead_tup,
ROUND(100.0 * n_dead_tup / NULLIF(n_live_tup + n_dead_tup, 0), 2) AS dead_pct,
last_autovacuum,
autovacuum_count,
EXTRACT(EPOCH FROM (now() - last_autovacuum)) / 3600 AS hours_since_vacuum
FROM pg_stat_user_tables
WHERE pg_total_relation_size(relid) > 40 * 1024^3
ORDER BY pg_total_relation_size(relid) DESC;
De score is simpel. Groen: dead_pct onder de 8, laatste autovacuum binnen 24 uur. Geel: dead_pct tussen 8 en 20, of laatste autovacuum meer dan 48 uur terug. Rood: alles boven de 20% dode tuples op een tabel waaruit de agent gaat lezen. Rood betekent dat we vacuum-tuning offreren als voorwaarde, niet als fase-twee-verbetering.
Twee patronen zien we steeds terug. Het eerste is een orders-tabel van 60 à 90 GB met de standaard scale factor en 14% dode tuples. Simpele fix: zet met een ALTER TABLE de scale factor op 0.02 en de threshold op 50.000, en laat 'm een weekend lang bijbenen. Het tweede is een audit_log-tabel waaruit nooit iemand verwijdert, dus die is nog nooit gevacuumed. Op dode tuples is die prima en op freezing een ramp. pg_class.relfrozenxid is de kolom om in de gaten te houden.
Als age(relfrozenxid) op een tabel binnen 100 miljoen van autovacuum_freeze_max_age zit, is je cluster één verkeerde week verwijderd van een nood-anti-wraparound-vacuum die de tabel lockt. Dat is niet de week waarin je een agent wil installeren.
pg_stat_statements-dekking op de top 30 queries
De tweede vraag die we beantwoord willen hebben: zien we agent-verkeer terug zodra we het toevoegen? pg_stat_statements is de goedkoopste observability die je kan kopen, en op elk Nederlands mkb-cluster dat we auditen staat 'ie uit, half-geïnstalleerd, of op een track-niveau dat de queries waar het ons om gaat mist.
Twee checks. Eén: zit hij echt in shared_preload_libraries en is hij aangemaakt in de database waar de agent op uitkomt? Twee: staat pg_stat_statements.max hoog genoeg dat de queries waar het ons om gaat niet zijn weggegooid? Default is 5000, prima totdat je ORM achtduizend verschillende prepared-statement-vormen genereert.
SELECT
calls,
ROUND(total_exec_time::numeric, 0) AS total_ms,
ROUND(mean_exec_time::numeric, 2) AS mean_ms,
rows,
LEFT(query, 120) AS query_head
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 30;
We scoren de top 30 op drie assen: herkennen we de query (staat 'ie ergens in de codebase gedocumenteerd, of is hij een spook), klopt de gemiddelde executietijd met wat de ontwikkelaar denkt dat 'ie doet, en is er één enkele query die meer dan 15% van de totale executietijd opslokt. Die laatste is de agent-risico-vlag. Een query die nu al 18% van de totale executietijd eet, eet 30% zodra de agent hem veertig keer per sessie voor de klant gaat afvuren.
Als pg_stat_statements niet geïnstalleerd is, installeren we hem tijdens de audit-window en komen we na 72 uur terug om het resultaat te lezen. Eerder offreren is gokken.
De cutover-survival-vraag
Hier wordt de audit uitgesproken. PostgreSQL 12 zit uit community-support. We kunnen 'm met een betaald extended-support-contract in leven houden, maar we gaan geen nieuwe agent bouwen op een database waarvan de security-patches nu via een leveranciersrelatie lopen die de klant niet heeft. Dus het derde deel van de audit vraagt: kunnen we dit cluster naar 17 verhuizen zonder ook maar één AVG-vereiste audit-log-rij te verliezen tijdens de switchover?
Het mechanisme is logische replicatie van 12 naar 17. We zetten een parallel 17-cluster op, repliceren, verifiëren, verleggen DNS, degraderen 12 naar read-only, en houden 'm twee weken aan. Het risico zit in de naad: rijen die naar 12 zijn geschreven nadat de publication-slot was aangemaakt maar voor de cutover klaar was. Voor een orders-tabel leid je die opnieuw af. Voor een audit_log-tabel die juridisch append-only en compleet moet zijn, mag je niet opnieuw afleiden.
De survival-score voor één tenant hangt van vier dingen af. Eén: heeft elke tabel die gerepliceerd moet worden een primary key of een REPLICA IDENTITY FULL? Twee: gebruikt de audit_log-tabel een server-side now()-default in plaats van een client-klok? Drie: zitten er unlogged tables in het kritieke pad? Vier: accepteert de applicatie een read-only window van 90 seconden tijdens de cutover?
Op de laatste veertien mkb's die we scoorden, vielen de antwoorden zo uiteen. Elf hadden minstens één kritieke tabel zonder primary key, meestal een koppeltabel die iemand in 2017 had toegevoegd en weer was vergeten. Acht hadden audit_log-timestamps die uit de applicatieserver kwamen, wat betekent dat klok-skew tussen twee app-bakken rijen over de cutover heen in de verkeerde volgorde kan zetten. Drie waren schoon genoeg dat we nul audit-log-rij-verlies konden beloven op een cutover in één avond. Aan die drie hebben we agressief geoffreerd.
Wat de drie overlevers gemeen hadden
Het patroon in de drie schone tenants was geen technische verfijning. Het was discipline die hier toevallig uitbetaalde. Alle drie hadden ze één backend-engineer die ergens tussen 2019 en 2022 op drie dingen had gestaan: elke tabel krijgt een synthetische primary key, elke timestamp wordt in de database gegenereerd, en de audit_log-tabel is per maand gepartitioneerd met een retentiebeleid dat in code wordt afgedwongen.
Geen van de drie engineers dacht aan logische replicatie toen ze die keuzes maakten. Ze dachten aan debugbaarheid, reproduceerbaarheid en een AVG-recht-op-vergetelheid-verzoek dat een keer was binnengekomen en hen had vernederd. De audit profiteerde van beslissingen die om andere redenen waren genomen. Dit is de saaie waarheid waarom sommige clusters makkelijk te retrofitten zijn en andere niet: de afstemming is bijna nooit gepland.
De elf die de survival-check niet haalden waren niet slordig. Ze waren het natuurlijke resultaat van zeven of acht jaar just ship it, gerund door teams van één of twee mensen. De remediation die we offreren is niet glamoureus: primary keys toevoegen aan vier tabellen, de timestamp-default naar de database verhuizen, een triggerloze BEFORE INSERT-check op monotoniciteit op de audit-tabel zetten, de twee grootste tabellen partitioneren. Meestal één sprint, soms twee.
De 47-minuten-versie van de checklist
Voor een prospect-call waarin we één uur en een read-only Postgres-rol hebben, draaien we 'm in deze volgorde:
- Bevestig de versie, de geïnstalleerde extensions en
shared_preload_libraries. - Draai de autovacuum-lag-query hierboven op tabellen groter dan 40 GB. Noteer
dead_pctenhours_since_vacuumper tabel. - Vergelijk
age(relfrozenxid)metautovacuum_freeze_max_agevoor diezelfde tabellen. - Haal de top 30 op uit pg_stat_statements. Vlag elke query die meer dan 15% van de totale executietijd opslokt.
- Voor elke tabel waar de voorgestelde agent op gaat schrijven: check op een primary key of
REPLICA IDENTITY FULL. - Specifiek voor de audit_log-tabel: check dat de timestamp-kolom een server-side default heeft en dat er geen triggers op staan die rijen onder replicatie kunnen herordenen.
- Bekijk visueel het aantal replication slots, de WAL-grootte en eventuele bestaande publications.
Vijf van die zeven items kunnen mislukken op manieren die het agent-project blokkeren. Geen ervan vereist dat we al weten wat de agent precies gaat doen. Dat is precies het punt: het cluster is óf klaar voor nieuw verkeer óf niet, en dat antwoord staat los van de LLM die je erop wil koppelen.
Wat we met de score doen
De gescoorde audit gaat als appendix van twee pagina's de offerte in. Vonden we rode items, dan worden die een vaste-prijs voorbereidingsfase die voor het agent-werk loopt, en is de agent-offerte voorwaardelijk aan de afronding ervan. Vonden we gele items, dan zetten we ze met een prijsband als risico in de offerte. Is alles groen, dan offreren we het agent-werk vlak en gaan we snel.
Toen we afgelopen kwartaal de klantenservice-agent bouwden voor een Brabantse groothandel, ving de audit een shipment_events-tabel van 72 GB op die op 26% dode tuples zat en negen dagen geen autovacuum had gehad. We hebben de eerste week autovacuum getuned en de tabel per maand gepartitioneerd voor we ook maar één agent-prompt schreven. De agent ging op tijd live omdat de database klaar was toen 'ie aankwam, en zo beginnen de meeste van onze AI-agent-projecten: niet met een modelbeslissing, maar met een SELECT op pg_stat_user_tables.
Doe je vandaag één ding, draai dan de autovacuum-lag-query hierboven tegen je eigen productie-replica en schrijf het slechtste dead_pct-getal op dat je ziet. Dat ene getal, op een geeltje boven je monitor, zegt meer over of jouw database klaar is voor een AI-agent dan welke leveranciersbrochure ook.
Kern
Draai de autovacuum-lag-query op je grootste tabellen voor je AI-agent-werk scopet. Het slechtste dead_pct-getal op je cluster is de echte gereedheidsscore.
FAQ
Waarom 40 GB als autovacuum-drempel?
Onder de 40 GB produceert de standaard scale factor van 0.2 vacuum-cycles die binnen een onderhoudswindow afronden. Daarboven wacht dezelfde instelling op 8 GB dode tuples voor hij draait, en dat botst met kantooruren.
Kunnen we niet gewoon op Postgres 12 blijven met extended support?
Dat kan, maar we offreren er geen nieuwe AI-agent bovenop. Security-patches achter een betaald contract zijn prima voor legacy-onderhoud en slecht voor nieuw bouwwerk waarvan de klant verwacht dat het gaat groeien.
Wat als pg_stat_statements niet geïnstalleerd is?
Dan installeren we 'm tijdens de audit-window en komen we na 72 uur terug. Zonder drie dagen representatieve data is de top-30-lijst ruis en is elke offerte die erop is gebaseerd een gok.
Hoe lang duurt de volledige audit?
Ongeveer 47 minuten op een gezond cluster met een werkende read-replica en een Postgres-rol die pg_stat_user_tables en pg_stat_statements mag lezen. Bijna een hele dag als extensions ontbreken of de stats verouderd zijn.