SaaS-architectuur
Eén klant exporteren met pg_dump en row-level security
pg_dump heeft geen WHERE, maar row-level security kan er een zijn. Getest op PostgreSQL 17: rijen van één klant, lekkende tellerstanden, terugzetten onder RLS.
Een klant vraagt om een kopie van al zijn gegevens, of je wilt één klant naar een andere database verhuizen. In een multi-tenant SaaS waarin alle klanten dezelfde tabellen delen, pak je als eerste pg_dump — en dat heeft geen --where-optie. Het dumpt een tabel, of niet.
Als je klanten gescheiden zijn met row-level security, heb je die WHERE al. We testten hoe ver je daarmee komt op PostgreSQL 17.10, in een wegwerpcontainer: 311.205 facturen en 1.244.820 factuurregels over 100 klanten, row-level security met FORCE op alle drie de tabellen, en een applicatierol app die geen eigenaar is. Het beleid leest de klant uit een sessievariabele, nullif(current_setting('app.tenant', true), '')::int.
Kan pg_dump alleen de rijen van één klant exporteren?
Ja, met --enable-row-security en de klant ingesteld voor de sessie van de dump:
PGOPTIONS="-c app.tenant=50" pg_dump -U app --data-only --enable-row-security \
-t tenants -t invoices -t invoice_lines -f klant-50.sql
Wat eruit kwam:
Als app | Resultaat |
|---|---|
zonder --enable-row-security | ERROR: query would be affected by row-level security policy for table "tenants" |
| met, zonder klant | drie lege tabellen, geen fout |
| met, klant 50 | precies klant 50: 1 klant, 1.200 facturen, 4.800 regels; 0,09 s, 453 kB |
| met, klant 1 (de grootste) | 1 klant, 60.000 facturen, 240.000 regels; 0,24 s, 21,9 MB |
De eerste regel is bewust: standaard zet pg_dump row security uit “to ensure that all data is dumped”, en weigert als dat niet mag. De tweede is die je in een script in de gaten houdt. Met een nullif-beleid betekent een ontbrekende klant nul rijen — ons RLS-artikel noemt dat “de goede kant op fout”, en voor query’s klopt dat. Voor een export is het een stille fout: een taak waarbij de klantvariabele niet aankwam, levert een geldig, leeg bestand op. Tel de rijen in wat je overhandigt.
pg_dump “makes consistent backups even if the database is being used concurrently”: de facturen en de regels in de export komen uit één snapshot en horen bij elkaar, ook terwijl de klant doorwerkt. De tellerstanden hieronder zijn de uitzondering; die worden gelezen zoals ze op dat moment zijn.
Wat lekt er in een pg_dump van één klant?
De tellerstanden. De export van klant 50, met 1.200 facturen, eindigde met:
SELECT pg_catalog.setval('public.invoice_lines_id_seq', 1244820, true);
SELECT pg_catalog.setval('public.invoices_id_seq', 311205, true);
Dat zijn tellers die alle klanten delen. Geef je dit bestand aan klant 50, dan weet die hoeveel facturen en regels er in je hele product bestaan — een getal dat je misschien nergens publiceert. Row-level security geldt niet voor sequences, en pg_dump -t neemt de sequences mee die bij de gekozen tabellen horen. Beide sequences uitsluiten met -T invoices_id_seq -T invoice_lines_id_seq haalde ze er niet uit; --exclude-table-data='*_seq' wel. Dat patroon leunt op de standaardnaamgeving van sequences, dus toets het aan je eigen namen.
Zonder de sequencegegevens heeft pg_dump ook geen SELECT op de sequences meer nodig: mét stopte het met permission denied for sequence invoice_lines_id_seq, met --exclude-table-data='*_seq' liep het zonder dat recht. De applicatie zelf kan het totaal nog steeds zien — een nextval gaf 311.206 — maar de applicatie is de klant niet. Het lek zit in het overhandigen van het bestand.
Kun je de dump van een klant terugzetten onder row-level security?
Niet in het standaardformaat. Als app, met de klant ingesteld, stopte het inlezen bij de eerste tabel:
ERROR: COPY FROM not supported with row-level security
HINT: Use INSERT statements instead.
De COPY-documentatie zegt het ook, en de pg_dump-documentatie raadt bij --enable-row-security om die reden het INSERT-formaat aan. Met --inserts of --rows-per-insert ging het terugzetten door de WITH CHECK van het beleid, en dat wil je als het doel een gedeelde database is:
| Terugzetten van klant 1 (60.000 facturen, 240.000 regels), één transactie | Tijd | Grootte dump |
|---|---|---|
| COPY-formaat, als superuser | 9,1 s | 21,9 MB |
--rows-per-insert=1000, als app onder RLS | 11,3 s | 25,2 MB |
--inserts (één rij per opdracht), als app onder RLS | 30,2 s | 36,6 MB |
De superuser-regel staat er als maatstaf, niet als gelijke vergelijking: die slaat het beleid over. Alle drie de dumps voor het terugzetten als app zijn gemaakt met --exclude-table-data='*_seq'. Klant 50 met --rows-per-insert=1000 kostte 0,24 s. Met de klantvariabele op een andere klant weigerde het terugzetten al bij de eerste rij: new row violates row-level security policy for table "tenants". De database weigert het bestand van de ene klant bij een andere.
Wat de setval met een live database doet
Hiermee gaat productie een tijdje stuk. De dump van een klant draagt de id’s van zijn rijen mee en de stand van de gedeelde teller op het moment van de dump. Zet je hem als superuser terug in de live database — bijvoorbeeld na een per ongeluk verwijderde klant — dan zet de setval de teller terug. We voegden een factuur toe (id 311.206), draaiden de setval-regel uit de eerdere dump, en voegden er nog een toe:
ERROR: duplicate key value violates unique constraint "invoices_pkey"
DETAIL: Key (id)=(311206) already exists.
Elke mislukte insert verbruikt toch een nummer, dus nieuwe facturen van elke klant falen tot de teller alle id’s voorbij is die sinds de dump zijn uitgedeeld — één in onze test, duizenden op een drukke dag. Als app werd dezelfde setval geweigerd (permission denied for sequence invoices_id_seq, want de applicatie heeft alleen USAGE), en dat is de veilige uitkomst; het gevaar is terugzetten als superuser. Laat de sequencegegevens weg uit klantexports — dezelfde --exclude-table-data='*_seq' — en het probleem ontstaat niet.
Een checklist voor het exporteren van één klant
pg_dump --enable-row-securityals applicatierol, met de klant inPGOPTIONS. Nooit als een rol die row security omzeilt.- Controleer de aantallen rijen tegen de database voordat je iets overhandigt. Een lege export is een geldig bestand.
--exclude-table-data='*_seq': gedeelde tellers zijn geen gegevens van de klant, en terugzetten is niet veilig.--rows-per-insertals het bestand onder row-level security terug moet; COPY kan dat niet.- Terugzetten met de klant ingesteld, in één transactie (
psql -1), zodat een verkeerde klant of een dubbele rij het hele terugzetten terugdraait.
Hoe lang het verwijderen van de klant daarna duurt, en wat er dan op de schijf achterblijft, staat in één klant verwijderen in PostgreSQL.
Wat we niet hebben gemeten
Het custom- en directoryformaat (-Fc, -Fd) met pg_restore, tabellen zonder tenant_id-kolom die toch bij een klant horen (koppeltabellen, bestanden), een rol per klant in plaats van een sessievariabele, en terugzetten in een database waar de id’s van de klant al door een ander gebruikt worden.
Bronnen: de PostgreSQL 17-documentatie over pg_dump (--enable-row-security, consistentie, --exclude-table-data) en COPY (“COPY FROM is not supported for tables with row-level security”), gelezen op 1 oktober 2026. Alle resultaten zijn van onszelf, op PostgreSQL 17.10, op 1 oktober 2026.