SaaS-architectuur
Bedragen in PostgreSQL: numeric, integer of money?
Welk PostgreSQL-type bewaart geld exact? numeric, centen in integer of bigint en money naast elkaar, met de valkuilen die we testten en een controlequery.
Now-Next bouwt SaaS-producten en neemt bestaande over. Wat we bouwen
In bedragen in centen, geen floats leggen we uit waarom een komma-getal geen geld kan bewaren. Dan blijft de praktische vraag over die elk PostgreSQL-schema moet beantwoorden: welk type gebruik je dan wél? Er zijn drie serieuze kandidaten — numeric, een geheel aantal centen in integer of bigint, en het ingebouwde type money — en de discussie erover is meestal luider dan het verschil.
Dit artikel zet ze naast elkaar, met het gedrag dat we zelf in psql hebben nagelopen en met wat onze eigen twee producten werkelijk gebruiken. Dat laatste doet ertoe: wij bewaren niet elk bedrag in centen, en we leggen uit waarom.
Welk PostgreSQL-type gebruik je voor bedragen?
Voor de meeste SaaS-producten is numeric met een expliciete precisie en schaal het juiste type voor geld — numeric(14,2) voor bedragen, en meer decimalen voor stukprijzen en tarieven — omdat het exact is, SQL-rapportages er direct mee kunnen rekenen en elke kolom het aantal decimalen krijgt dat hij nodig heeft. Een geheel getal in de kleinste munteenheid (bigint in centen) is net zo exact en sneller, en past beter als bedragen alleen worden opgeteld en vooral tussen applicatiecode en een betaalsysteem heen en weer gaan. Gebruik geen real of double precision, want die zijn niet exact, en geen money, want dat kan geen fracties van een cent bewaren en koppelt zijn decimalen en opmaak aan de database-instelling lc_monetary.
De documentatie van PostgreSQL zegt hetzelfde over numeric: het wordt “especially recommended for storing monetary amounts and other quantities where exactness is required”.
De typen in één tabel
| Type | Exact | Fracties van een cent (stukprijzen, tarieven) | Wat delen in SQL doet | In de applicatie | Grootste valkuil |
|---|---|---|---|---|---|
real / double precision | nee | bij benadering | een benaderde breuk | een gewoon getal, al onnauwkeurig | 0.1 + 0.2 geeft 0.30000000000000004 |
numeric(p,s) | ja | ja, zover de schaal reikt, bijv. numeric(12,4) | een exacte breuk met veel decimalen | hangt af van de driver; Prisma geeft een Decimal.js-object | een waarde met meer decimalen dan de kolom wordt bij opslaan afgerond, zonder foutmelding |
integer in centen | ja | nee, alleen met een tweede afspraak | kapt af richting nul: 1000 / 3 geeft 333 | past in een JavaScript-getal | het maximum is 2.147.483.647 cent: € 21.474.836,47 |
bigint in centen | ja | nee, alleen met een tweede afspraak | kapt af richting nul | node-postgres geeft het terug als tekst | boven Number.MAX_SAFE_INTEGER is een JavaScript-getal niet meer exact |
money | ja, voor zijn vaste decimalen | nee | money / integer kapt af; money / money geeft double precision | opgemaakte tekst zoals $3.33 | decimalen en opmaak volgen de database-instelling lc_monetary |
De kolom die het vaakst beslist is fracties van een cent. Zodra je een stukprijs van € 0,0125 hebt, een brandstofprijs per liter of een btw-tarief van 21,000, hebben centen een tweede regel nodig voor die getallen, en numeric niet.
Heeft de kolom fracties van een cent nodig — een stukprijs, een tarief, een prijs per liter?
- Ja
numericmet de schaal die hij nodig heeft, bijvoorbeeldnumeric(12,4)Hele centen hebben voor deze getallen een tweede afspraak nodig. - Nee
Wordt het bedrag vermenigvuldigd, verdeeld of met verschillende precisies gerapporteerd?
- Ja
numericmet een expliciete schaal, bijvoorbeeldnumeric(14,2)Exact, en SQL-rapportages kunnen er direct mee rekenen. Een waarde met meer decimalen wordt bij opslaan afgerond, zonder foutmelding. - Nee
Het wordt alleen opgeteld en doorgegeven. Kan een totaal boven € 21.474.836,47 uitkomen?
- Ja
bigintin centennode-postgres geeft het standaard terug als tekst. - Nee
integerin centenPrima voor prijzen en abonnementen; boven het maximum volgt de foutinteger out of range.
- Ja
- Ja
Niet in de boom: real en double precision (niet exact), en money (geen fracties van een cent, decimalen volgen lc_monetary).
numeric: exact, en hij rondt af zonder het te zeggen
numeric bewaart decimale cijfers in plaats van machten van twee, dus 0.1 + 0.2 is gewoon 0.3. Optellen, aftrekken en vermenigvuldigen zijn exact. De prijs is snelheid: volgens de PostgreSQL-documentatie zijn berekeningen met numeric “very slow compared to the integer types”. Hoe traag staat verderop gemeten: 1,3 tot 3 keer trager dan bigint op hele query’s over 10 miljoen rijen, met één uitzondering van 9 keer die aan de sortering ligt en niet aan het rekenwerk. Een product dat de factuurregels van één klant optelt, merkt er niets van.
Drie dingen die we op PostgreSQL 16.14 hebben nagelopen, en die je wilt weten voordat je erop leunt:
- Een kolom met een schaal rondt af bij het opslaan.
2.345in een kolomnumeric(12,2)wordt2.35, zonder waarschuwing en zonder fout. De documentatie bevestigt het: heeft de waarde meer decimalen dan de kolom, dan “the system will round the value”. Een btw-bedrag dat de applicatie met vier decimalen uitrekent, komt dus al afgerond in de database — één keer, op een plek die niemand heeft gekozen. numericzonder precisie en schaal accepteert elk aantal decimalen. De documentatie noemt dat een “unconstrained numeric”-kolom. Dan houdt niets een bedrag van10.3333333333333333tegen. Geef een geldkolom dus altijd een schaal.NaNpast erin.numerickent de speciale waardenNaN,Infinityen-Infinity. In onze test weigerde eennumeric(12,2)welInfinity, maar accepteerde hij'NaN'. Een bedragkolom verdient eenCHECK-constraint die dat uitsluit.
En ook numeric lost het delen niet op. 10.00 / 3 geeft 3.3333333333333333; rond je elk van de drie delen af op centen en tel je ze op, dan kom je op 9.99. De verdwenen cent is geen typeprobleem maar een verdeelprobleem, en de oplossing — naar beneden verdelen en de resterende centen uitdelen aan de grootste resten — staat in het artikel over centen.
Centen in een integer: snel, en delen gooit de rest weg
Een geheel aantal centen is exact en zo snel als een getal maar kan zijn, en zolang het onder de grenzen hieronder blijft, past het in een gewoon JavaScript-getal. Er horen drie grenzen bij:
integerstopt bij € 21.474.836,47. Het maximum van eenintegervan vier bytes is 2.147.483.647, en in centen is dat ruim 21 miljoen euro. Een maandabonnement haalt dat nooit; een jaartotaal over alle klanten wel. Overschrijden geeft de foutinteger out of range— het gaat dus niet stil mis, maar in productie. Gebruik voor totalenbigint.bigintpast niet in een JavaScript-getal.Number.MAX_SAFE_INTEGERis 9.007.199.254.740.991; daarboven is een JavaScript-getal niet meer exact. Daarom geeft node-postgres eenbigintstandaard als tekst terug. In centen ligt die grens bij ongeveer 90 biljoen euro, dus het bedrag zelf is veilig — maar code die overal zonder nadenkenNumber(row.total)doet, heeft de garantie wel losgelaten.- Delen in SQL kapt af.
1000 / 3geeft333, en-1000 / 3geeft-333: de rest verdwijnt gewoon, volgens de documentatie “towards zero”. Een rapport dat in SQL bedragen verdeelt, kan op elke regel een cent kwijtraken zonder foutmelding.
En een stukprijs met vier decimalen past niet in centen. Dan heb je een tweede kolom met een andere eenheid nodig, of een afspraak welke kolommen in honderdsten van een cent tellen — en dat is precies het soort tweede vorm van een bedrag dat iemand uiteindelijk bij de eerste optelt.
Het type money: waarom de PostgreSQL-wiki het afraadt
money lijkt de voor de hand liggende keuze, en het is het enige van de drie typen waarvan de PostgreSQL-wiki uitdrukkelijk zegt dat je het niet moet gebruiken. Het aantal decimalen “is determined by the database’s lc_monetary setting”, de uitvoer wordt voor die taalinstelling opgemaakt, en volgens de documentatie “might not work” het terugzetten van een dump in een database met een andere lc_monetary. Deel je money door een integer, dan wordt er afgekapt, en money gedeeld door money geeft een double precision — precies het type dat je wilde vermijden.
De wikipagina Don’t Do This is er duidelijk over: money “doesn’t handle fractions of a cent”, en de afronding “is probably not what you want”. Er staat één uitzondering bij: één munt, geen fracties van een cent, en alleen optellen en aftrekken. Weinig SaaS-producten blijven lang binnen die grenzen.
Wat gebeurt er met een money-kolom als lc_monetary verandert?
De opgeslagen waarde verandert niet, maar wat hij betekent wel: een money-kolom bevat een geheel aantal van de kleinste eenheid, en lc_monetary bepaalt bij elke lees- en schrijfactie hoeveel van de cijfers decimalen zijn. We sloegen 12.34 op in een money-kolom op PostgreSQL 17.11 met lc_monetary op en_US.utf8, wijzigden daarna alleen die instelling in de sessie en lazen dezelfde rij opnieuw (13 oktober 2026):
lc_monetary | De rij die als $12.34 is opgeslagen leest als | amount::numeric | Wat de tekstliteral '12.34' wordt bij een insert |
|---|---|---|---|
en_US.utf8 | $12.34 | 12.34 | $12.34 |
nl_NL.UTF-8 | € 12,34 | 12.34 | € 1.234,00: de punt valt weg als scheidingsteken voor duizendtallen |
ja_JP.UTF-8 | ¥1,234 | 1234 | ¥12: afgerond op hele yen |
ar_BH.UTF-8 (drie decimalen) | د.ب. 1.234 | 1.234 | د.ب. 12.340 |
Geen van de lees- en schrijfacties in deze tabel gaf een fout of een waarschuwing. lc_monetary is een gewone instelling (context = user in pg_settings) die een rol voor haar eigen sessie mag wijzigen, naar elke locale die op de server is geïnstalleerd; een rol met alleen SELECT en INSERT op de tabel deed dat in onze test.
Wat er misgaat hangt af van de locale. Onder de locale zonder decimalen las elk bedrag dat met twee decimalen was geschreven een factor honderd te groot, en ging elke invoer fout: de tekst '12.34', de waarde 12.34::numeric en het gehele getal 12 kwamen alle drie uit op wat en_US.utf8 leest als $0.12. Onder de locale met een decimale komma lazen de opgeslagen bedragen goed en werd een numeric-waarde goed opgeslagen; alleen tekstinvoer ging fout, omdat de punt wegvalt: '12.34' werd honderd keer te groot opgeslagen, '1.5' tien keer. Het omgekeerde gebeurt ook: met en_US.utf8 werd de literal '12,34' opgeslagen als $1,234.00.
Bij een dump gaat het wél hardop mis. pg_dump schrijft de kolom als tekst, opgemaakt met de lc_monetary van de dumpsessie ($12.34 en $1,000.00 onder en_US.utf8; dezelfde rijen kwamen eruit als ¥1,234 en ¥100,000 toen we onder ja_JP.UTF-8 dumpten), en legt geen lc_monetary in de dump vast. De dump van en_US.utf8 laden met lc_monetary op nl_NL.UTF-8 mislukte met invalid input syntax for type money: "$12.34". Dat is het geval waarvoor de documentatie waarschuwt, en het is de betere van de twee manieren om fout te gaan, omdat er een fout te zien is. De restore stopte er alleen niet door: psql ging verder, eindigde met exitcode 0 en liet de tabel leeg achter. Draai een restore met ON_ERROR_STOP.
Het type heeft bovendien drie afrondingsregels. Delen door een geheel getal kapt af: '10.00'::money / 3 gaf $3.33 en '0.15'::money / 2 gaf $0.07. Vermenigvuldigen met of delen door een breuk rondt een half af naar de even cent: '0.05'::money * 0.5 gaf $0.02, '0.15'::money * 0.5 gaf $0.08 en '0.15'::money / 2.0 gaf $0.08. Invoer rondt een half van nul af: '0.025'::money gaf $0.03. En '10.00'::money / '3.00'::money gaf de double precision-waarde 3.3333333333333335.
Hoeveel trager is numeric dan bigint?
In onze meting deed een hele query over 10 miljoen rijen er op numeric(14,2) 1,3 tot 2 keer zo lang over als op bigint in centen, en 3 keer zo lang als elke rij wordt vermenigvuldigd en afgerond. Eén query viel daarbuiten: een sortering met een grote LIMIT of OFFSET was 9 keer trager, om een reden die hieronder staat. Op schijf waren de twee typen in de ene tabelindeling even groot en in de andere 16 procent verschillend, en een index over herhaalde bedragen was op numeric vier keer zo groot. Gemeten op 13 oktober 2026 op PostgreSQL 17.11.
De opstelling: vijf tabellen met dezelfde 10 miljoen bedragen, gelijkmatig getrokken tussen 0,02 en 99.999,98, zodat negen op de tien vijf cijfers voor de komma hebben. Elke tabel heeft een bigint-id, een integer-klant met 500 waarden en één bedragkolom. Eén proces, geen parallelle workers, JIT uit, work_mem op 2 GB zodat elke sortering en hash in het geheugen blijft, tabellen bevroren en in de cache (shared_buffers 1 GB, de rest in de cache van het besturingssysteem). De tijden zijn de uitvoeringstijd uit EXPLAIN (ANALYZE, TIMING OFF): de rijen worden gemaakt maar niet naar een client gestuurd. Elke tijd is de mediaan van zeven runs (vijf voor de volledige sortering en de top tien). De index is per type één keer gebouwd, met maintenance_work_mem op 1 GB en zonder parallelle workers. De machine is een desktop met een achtkernige Ryzen 7 3700X.
| Query over 10 miljoen rijen | numeric(14,2) | bigint centen | integer centen | double precision | money |
|---|---|---|---|---|---|
sum(amount) | 960 ms | 608 ms | 570 ms | 635 ms | 609 ms |
sum(amount) per klant (GROUP BY, 500 groepen) | 1.802 ms | 1.419 ms | 1.386 ms | 1.402 ms | 1.384 ms |
count(*) waar het bedrag boven 50.000 ligt | 1.079 ms | 631 ms | niet gemeten | 627 ms | niet gemeten |
| 21% btw per rij, afgerond op de cent, opgeteld | 2.279 ms | 736 ms | niet gemeten | 6.158 ms | niet gemeten |
ORDER BY amount, alle rijen | 3.991 ms | 2.005 ms | 2.034 ms | 2.678 ms | 3.002 ms |
ORDER BY amount DESC LIMIT 10 | 1.290 ms | 674 ms | niet gemeten | 665 ms | niet gemeten |
ORDER BY amount OFFSET 9999999 LIMIT 1 | 17.760 ms | 1.972 ms | 1.997 ms | 2.638 ms | 2.977 ms |
CREATE INDEX op (tenant, amount), één run | 12,5 s | 6,3 s | 5,9 s | niet gemeten | niet gemeten |
Dit zijn verhoudingen van hele query’s. Elk ervan betaalt ook ongeveer 425 ms alleen al om de 10 miljoen rijen langs te lopen (een kale count(*), gelijk op de numeric- en de bigint-tabel). Trek je dat eraf, dan is het optellen zelf op numeric 2,9 keer trager; hoe meer een query doet dat niets met het bedrag te maken heeft, hoe kleiner het verschil wordt. Met de standaardinstellingen van PostgreSQL (twee parallelle workers naast de leider, JIT aan) duurde dezelfde sum 354 ms tegen 237 ms: ongeveer dezelfde verhouding, minder milliseconden voor beide. money sorteert trager dan bigint, hoewel het dezelfde acht bytes zijn; waarom hebben we niet uitgezocht.
Die sortering daargelaten verschilt de btw-rij het meest, en dat is het werk dat een grootboek de hele dag doet. In numeric is het round(amount * 0.21, 2). In centen is het (amount * 21 + 50) / 100. Beide gaven over de 10 miljoen rijen hetzelfde totaal, 105.015.588.888,10. De variant in gehele getallen rondt alleen voor positieve bedragen half naar boven af: voor een bedrag van −0,50 geeft hij −0,10 waar round −0,11 geeft, dus een creditnota heeft een eigen tak nodig.
In double precision bestaat er geen round met een aantal decimalen: round(double precision, integer) bestaat niet, dus de waarde moet eerst naar numeric worden omgezet, en door die omzetting is het onnauwkeurige type in die rij het traagst. De variant zonder omzetting, round(amount * 100 * 0.21) / 100, gaf een totaal van 105.015.588.386,86, en dat is € 501,24 te weinig: round op een double precision rondt een half af naar het dichtstbijzijnde even getal (round(2.5) geeft daar 2, en 3 op numeric).
Voor een product dat de regels van één klant optelt, is het verschil in rekenwerk niet te merken: de 20.000 rijen van één klant optellen via de index duurde 3,8 ms op numeric en 3,0 ms op bigint. Het gaat tellen in een rapport over alle klanten tegelijk, bij het bouwen van een index op een grote tabel, en in de sortering hieronder.
Waarom was de sortering met OFFSET negen keer trager?
De laatste rij ophalen met ORDER BY amount OFFSET 9999999 LIMIT 1 duurde 17,8 s op de numeric-kolom, terwijl alle rijen sorteren zonder limiet 4,0 s duurde. PostgreSQL sorteert numeric snel door eerst een compacte afkorting van elke waarde te vergelijken, acht bytes op een 64-bits build, en zet dat uit voor een sortering met een grens: de broncode zegt “Bounded sorts are not an effective target for abbreviated key optimization” (tuplesort.c, PostgreSQL 17). Een bigint wordt direct vergeleken en heeft geen afkorting te verliezen; daarom verandert zijn tijd niet (2,0 s in beide gevallen).
Een top tien merkt er weinig van (1,9 keer in de tabel, net als de volledige sortering). De kosten groeien met de grens: op een tabel van één miljoen rijen duurde alles sorteren 305 ms op numeric, LIMIT 100000 617 ms en LIMIT 1000000 1.302 ms, terwijl op bigint de limiet van 100.000 sneller was dan de volledige sortering (149 ms tegen 184 ms). Diep pagineren en grote limieten over een bedragkolom betalen dit.
Neemt numeric meer schijfruimte in dan bigint?
Een numeric-bedrag neemt 3 bytes in voor nul en 11 voor een bedrag van twaalf cijfers met centen, tegen vast 8 voor bigint en 4 voor integer. numeric bewaart een kop van drie bytes plus twee bytes voor elke groep van vier decimale cijfers, geteld vanaf de komma, en laat groepen die helemaal nul zijn aan het eind weg. De grootte volgt dus de cijfers, niet de omvang van het getal:
Waarde in een numeric(20,2)-kolom | Opgeslagen bytes |
|---|---|
0.00 | 3 |
1.00, 0.01, 0.50, 100.00, 5000.00, 10000.00, 100000000.00 | 5 |
12.34, 99.99, 100.50, 9999.99 | 7 |
10000.01, 99999.99, 12345678.90 | 9 |
100000000.01, 123456789012.34 | 11 |
De 10 miljoen bedragen in onze tabellen namen 5 tot 9 bytes in, gemiddeld 8,8: 89 procent ervan is 10.000,00 of meer met centen, en die nemen er negen. De tabellen volgden dat getal niet:
| Tabelindeling | numeric(14,2) | bigint | integer |
|---|---|---|---|
id bigint, tenant integer, amount: 10 miljoen rijen | 498 MB | 498 MB | 422 MB |
id bigint, amount: 1 miljoen rijen | 49,5 MB | 42,5 MB | 42,5 MB |
Index op (tenant, amount): 10 miljoen rijen, bedragen herhalen nauwelijks | 301 MB | 301 MB | 214 MB |
Index op (tenant, amount): 2 miljoen rijen, 200 verschillende prijzen | 60 MB | 14 MB | niet gemeten |
MB is wat pg_size_pretty meldt, 1.048.576 bytes. In de eerste indeling zijn de numeric- en de bigint-tabel tot op de byte even groot, en in de tweede bespaart integer niets ten opzichte van bigint. PostgreSQL vult elke rij aan tot een veelvoud van acht bytes en lijnt een bigint uit op acht, terwijl een korte numeric geen uitlijning nodig heeft. Na een tenant van vier bytes verspilt de bigint vier bytes en gebruikt de numeric ze; na een id van acht bytes loopt een numeric van negen bytes één byte over in de volgende acht. Het type van een kolom en zijn plek beslissen samen.
De laatste rij is degene die ertoe kan doen. Een B-tree bewaart een herhaalde sleutel één keer met een lijst rijverwijzingen, en volgens de PostgreSQL-documentatie geldt “numeric cannot use deduplication”, omdat gelijke waarden in weergaveschaal kunnen verschillen. Onze willekeurige bedragen herhalen binnen een klant nauwelijks, dus de eerste index is voor beide typen even groot. Met 200 verschillende prijzen over 2 miljoen rijen was dezelfde index 60 MB op numeric en 14 MB op bigint.
Wat een som over double precision teruggeeft
Dezelfde 10 miljoen bedragen tellen op tot precies 500.074.230.467,84. De kolommen in numeric, bigint, integer en money gaven dat terug. De kolom in double precision gaf een ander totaal telkens als het plan of de rijvolgorde veranderde, en met een parallel plan bij elke run:
| Hoe de som draaide | Uitkomst |
|---|---|
| Eén proces, tabelvolgorde, drie runs | telkens 500.074.230.467,91376 |
| Eén proces, rijen aangeleverd in omgekeerde id-volgorde | 500.074.230.467,7946 |
| Twee parallelle workers naast de leider, acht runs | acht totalen, van 500.074.230.467,8259 tot 500.074.230.467,86127 |
Zeven cent te veel in tabelvolgorde, ongeveer vijf te weinig in omgekeerde volgorde, en bij elke parallelle run een andere fractie van een cent. Een som in drijvende komma hangt af van de volgorde van de optellingen, en een parallel plan legt die volgorde niet vast: de workers nemen de blokken zoals ze komen. Een totaal dat tussen twee runs van dezelfde query verandert, is de praktische vorm van het argument in bedragen in centen, geen floats.
sum over een bigint-kolom geeft numeric terug
sum(integer) geeft bigint, maar sum(bigint) geeft numeric, en daarin past elk totaal. Het totaal komt dus als numeric in de applicatie aan en niet in het type van de kolom: code die hele centen in het kolomtype verwacht, krijgt een decimaal. De kosten in de database zijn klein, 608 ms tegen 570 ms voor sum(integer) in de tabel hierboven.
Wat we in onze eigen producten gebruiken
Onze beide producten draaien op PostgreSQL, en geen van beide bewaart al het geld in centen.
booxx, onze Nederlandse boekhouding, bewaart het grootboek in numeric(14,2): debit en credit per journaalregel. Factuurregels hebben een unit_price in numeric(12,4) en een quantity in numeric(12,3), en btw-tarieven staan in numeric(6,3). Een boekhoudproduct heeft verschillende schalen naast elkaar nodig, en het grootboek spreekt dezelfde taal als de rapportages en de aangifte: euro’s met twee decimalen.
De billing-laag van booxx — wat een administratiekantoor per maand ontvangt of betaalt, en wat een abonnement per administratie kost — gebruikt wél hele centen in een integer. De migratie zegt in één regel waarom: bedragen in centen (integer) — dit is de billing-laag, niet de boekhouding. Dat zijn vaste prijzen die alleen worden opgeteld, en daar wint het eenvoudigere type.
Pilot-Next, ons product voor vliegclubs, bewaart factuurtotalen, btw-bedragen en het saldo na elke transactie in numeric(12,2), via Prisma. In de applicatie komen die waarden binnen als Decimal.js-objecten en niet als JavaScript-getallen, zodat de exactheid de stap van database naar code overleeft.
De regel achter beide keuzes: het type volgt wat het bedrag moet doen. Bedragen die worden vermenigvuldigd, verdeeld en met verschillende precisies gerapporteerd, horen in numeric met een expliciete schaal. Bedragen die alleen worden geteld en doorgegeven, horen in hele centen. En in beide gevallen is er in de code één type dat weet dat het geld is.
Controleer je eigen database met één query
Deze query toont de kolommen die een tweede blik verdienen: kolommen met komma-getallen of money, numeric-kolommen zonder schaal, en integers van vier bytes waarvan de naam op een bedrag wijst.
SELECT table_name, column_name, data_type, numeric_precision, numeric_scale
FROM information_schema.columns
WHERE table_schema NOT IN ('pg_catalog', 'information_schema')
AND (
data_type IN ('real', 'double precision', 'money')
OR (data_type = 'numeric' AND numeric_scale IS NULL)
OR (data_type = 'integer' AND column_name ~ '(amount|cents|price|total|balance)')
)
ORDER BY table_name, column_name;
Wat je met de uitkomst doet:
realofdouble precisionmet een bedrag erin is het enige echte noodgeval. Elke rij kan al een afwijking bevatten, en later omzetten betekent beslissen welke waarde de juiste was.moneywerkt zolang je binnen één munt blijft en niet deelt. Plan de overstap naarnumericvoordat dat niet meer waar is.numericzonder schaal is exact, maar houdt een onafgeronde waarde niet tegen. Geef de kolom een schaal zodra je weet welke.integerin centen is prima voor prijzen en abonnementen. Kijk voor totalen en saldi die groeien of 21 miljoen euro echt buiten bereik ligt.
Pas de kolomnamen in de laatste voorwaarde aan op je eigen naamgeving; in een Nederlands schema heten ze misschien bedrag of saldo.
Kun je het type later nog veranderen?
Technisch is het één ALTER TABLE … ALTER COLUMN … TYPE. In de praktijk zit de moeilijkheid niet in de opdracht maar in de data: een kolom double precision omzetten naar numeric(14,2) rondt waarden af die al een fractie verkeerd waren, en bij elke factuur die de deur uit is, is de vraag welk getal klopte — het opgeslagen totaal of de som van de regels. Daarom is geld een van de drie SaaS-beslissingen die je later niet terugdraait.
Wil je dat iemand anders deze keuzes vanaf de eerste migratie maakt: dat is precies het werk waarvoor wij een SaaS-product bouwen.
Het kolomtype is één van de zes dingen die een multi-tenant product in zijn database vastlegt; de andere vijf, en welke daarvan je later nog kunt wijzigen, staan in multi-tenant SaaS op PostgreSQL: zes beslissingen.
De andere kolom op een factuur die PostgreSQL niet vanzelf goed doet, is het nummer: hoe je elke klant een eigen reeks geeft zonder dubbelen of gaten, onder belasting nagemeten, staat in factuurnummers per klant in PostgreSQL, zonder gaten.