Zahlen in PostgreSQL sind nicht „einfach Zahlen”. Drei Familien stehen zur Wahl: Ganzzahlen (smallint, integer, bigint), exakte Dezimalzahlen (numeric/decimal) und Gleitkomma-Zahlen (real, double precision). Welcher Typ passt, hängt davon ab, wie groß die Werte werden, wie genau sie sein müssen und ob du sie addieren willst, ohne Cents zu verlieren.
Die drei Familien auf einen Blick
| Familie | Typen | Wertebereich | Genauigkeit | Speicher |
|---|---|---|---|---|
| Ganzzahl | smallint, integer, bigint | begrenzt, je nach Typ | exakt | 2 / 4 / 8 Bytes |
| Exakt | numeric, decimal | beliebig groß | exakt | variabel (~10–20 Bytes typisch) |
| Gleitkomma | real, double precision | sehr groß | nicht exakt | 4 / 8 Bytes |
Ganzzahlen — int, bigint und ihre Grenzen
| Typ | Alias | Wertebereich | Wann nehmen? |
|---|---|---|---|
smallint | int2 | −32 768 … +32 767 | Nur wenn der Wertebereich wirklich klein ist (Statuscodes, kleine Zähler). Selten lohnend. |
integer | int, int4 | ca. ±2,147 Mrd. | Standard. Für IDs, Mengen, Counts in fast jeder Anwendung. |
bigint | int8 | ca. ±9,2 · 10¹⁸ | Wenn IDs den Milliardenbereich erreichen oder Aggregat-Sums groß werden. |
Beispiel — eine Tabelle mit den passenden Typen:
CREATE TABLE orders (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
customer_id bigint NOT NULL, -- bigint, weil Kundenzahl wachsen kann
quantity integer NOT NULL, -- int reicht; max ~2 Mrd. ist Overkill
priority smallint DEFAULT 0, -- 0..10, smallint genug
view_count bigint DEFAULT 0 -- view_count summiert; bigint zur Sicherheit
);Faustregel: Für IDs in modernen Setups direkt bigint. Speicherkosten ist 4 Bytes mehr pro Zeile — bei 100 Mio. Zeilen also 400 MB extra; vernachlässigbar gegenüber dem Ärger eines IDs-laufen-aus-Migrationsprojekts. Twitter, Instagram, Stripe — alle wollten irgendwann von int auf bigint, einige unter Schmerzen.
Overflow — was passiert bei zu großen Werten?
myapp=> SELECT 2147483647::integer + 1;
ERROR: integer out of rangePostgres wirft einen Fehler — anders als manche andere Sprachen, in denen ein Overflow stillschweigend zu einer negativen Zahl wird. Das ist gut: Bugs werden sichtbar statt unbemerkt.
In Aggregaten kann das gefährlich werden:
-- amount ist int. Bei vielen großen Werten:
SELECT sum(amount) FROM big_payments;
-- ERROR: integer out of rangeWorkaround: explizit casten. sum(amount::bigint) oder direkt bigint als Spalten-Typ wählen. sum() über numeric läuft sowieso überlauf-frei.
numeric / decimal — exakte Dezimalzahlen
numeric(p, s) ist der Typ für Werte, die exakt sein müssen. p ist die Gesamtzahl Stellen, s die Stellen nach dem Komma:
-- Geld: bis 99 999 999.99
price numeric(10, 2)
-- Steuersatz: 0.0000 bis 9.9999
tax_rate numeric(5, 4)
-- Beliebig groß und genau (ohne p/s):
scientific_value numericBeispiel-Sitzung mit Geldbeträgen:
myapp=> CREATE TABLE invoices (
id bigserial PRIMARY KEY,
net numeric(10,2),
vat numeric(10,2),
total numeric(10,2)
);
myapp=> INSERT INTO invoices (net, vat, total)
VALUES (199.99, 38.00, 237.99);
myapp=> SELECT sum(total) FROM invoices;
sum
--------
237.99Wichtig: numeric ist langsamer als int/bigint/float, weil Postgres die Arithmetik in Software emuliert (kein nativer CPU-Befehl). Das spielt für klassische Web-Apps mit ein paar tausend Berechnungen pro Sekunde keine Rolle. In Number-Crunching-Workloads merkst du den Unterschied.
Warum nicht float für Geld?
Das ist die Kardinalfalle:
myapp=> SELECT 0.1::real + 0.2::real;
?column?
----------------------
0.30000001192092896
myapp=> SELECT 0.1::numeric + 0.2::numeric;
?column?
----------
0.3real/double precision sind IEEE-754-Gleitkommazahlen — sie können viele Dezimalwerte (0.1, 0.2, …) gar nicht exakt darstellen. Bei jeder Rechnung entsteht ein winziger Fehler. Bei einer Buchhaltung mit Millionen Transaktionen summieren sich diese Mikro-Differenzen zu sichtbaren Cent-Unterschieden.
Gold-Regel: Geld in numeric. Punkt. Kein real, kein double precision.
real und double precision — wann sinnvoll?
Float-Typen sind richtig für:
- Wissenschaftliche Berechnungen, in denen kleine Rundungsfehler okay sind
- Sensor-Messwerte (Temperatur, Druck) — die Messung selbst ist ohnehin ungenauer als der Float
- Geo-Koordinaten (
double precisionreicht für Sub-Meter-Präzision) - Statistik / Machine-Learning-Features
CREATE TABLE sensor_readings (
id bigserial PRIMARY KEY,
sensor_id integer,
temperature real, -- 4 Bytes, ca. 6 Stellen
timestamp timestamptz NOT NULL
);real ist 4 Bytes mit ~6 Stellen Genauigkeit, double precision 8 Bytes mit ~15 Stellen. Bei Zweifel double precision — der Speicher-Mehrbedarf ist meist nicht der Engpass.
Integer-Division — die andere Falle
myapp=> SELECT 10 / 3;
?column?
----------
3
myapp=> SELECT 10 / 3.0;
?column?
----------------------
3.3333333333333333
myapp=> SELECT 10::numeric / 3;
?column?
----------------------
3.3333333333333333Wenn beide Operanden Ganzzahlen sind, ist auch das Ergebnis eine Ganzzahl — die Nachkommastellen werden abgeschnitten, nicht gerundet. Das ist SQL-Standard und in vielen Programmiersprachen genauso. Für „echte” Division mindestens einen Operanden auf numeric oder float casten.
Migration: int → bigint im laufenden Betrieb
Eine int-ID-Spalte später auf bigint zu erweitern, ist heikel — Postgres muss die Tabelle neu schreiben. Bei großen Tabellen Stunden bis Tage. Pattern:
-- 1. Neue bigint-Spalte hinzufügen (NULL-Default, instant):
ALTER TABLE orders ADD COLUMN id_new bigint;
-- 2. Werte kopieren in Batches (in eigenen Transaktionen):
UPDATE orders SET id_new = id WHERE id BETWEEN 1 AND 100000;
UPDATE orders SET id_new = id WHERE id BETWEEN 100001 AND 200000;
-- ...
-- 3. Sequenz auf bigint umstellen, neue Inserts schreiben in id_new
-- (über Trigger oder Code-Anpassung).
-- 4. Wenn alle Werte kopiert: alte id-Spalte droppen, id_new umbenennen.In Produktion macht das niemand händisch — entweder Tools wie pg_repack einsetzen oder Logical Replication zu einer neu aufgesetzten Tabelle. Die Lehre: früh bigint wählen.
Häufige Stolperfallen
Float für Geld — die teuerste Idee.
0.1 + 0.2 = 0.30000000000000004 ist kein Postgres-Bug, sondern IEEE-754. In Geld-Buchungen führt das zu summierten Mikro-Differenzen, die nach Monaten als „warum stimmen unsere Bücher nicht?”-Tickets auftauchen. Der Fix ist immer dasselbe: Migration zu numeric, oft zu spät. Direkt mit numeric(10,2) anfangen.
Integer-Division schneidet ab.
5 / 2 = 2, nicht 2.5. Wer einen Bruchteil will, mindestens einen Operanden in numeric oder float casten: 5::numeric / 2. Klassischer Bug in Reporting-Queries, wo „Durchschnitt” nicht das ergibt, was man erwartet.
serial ist int, bigserial ist bigint.
SERIAL (= 4 Bytes) reicht für ~2,1 Mrd. IDs. Klingt nach viel — bei 1.000 Inserts pro Sekunde aber nach 25 Tagen aufgebraucht (wenn Lücken durch Rollbacks entstehen, früher). BIGSERIAL / bigint GENERATED AS IDENTITY ist für jede neue Tabelle die sichere Wahl.
numeric ohne Argumente ist beliebig groß.
numeric ohne (p, s) akzeptiert jede beliebige Genauigkeit und Skalierung. Praktisch für unbegrenzte Werte (wissenschaftliche Konstanten), aber: keine Validierung. Wer Geld speichert, sollte numeric(p, s) mit klaren Grenzen wählen, sonst übersieht man Eingabefehler („Tippe 999999999 statt 999.99”).
Casting vs. CAST — beide gehen.
'42'::integer und CAST('42' AS integer) sind identisch. Postgres-Code-Bases nutzen meistens ::, weil’s kürzer ist. Wer auf SQL-Standard-Konformität achtet (z. B. für Cross-DB-Tools), schreibt CAST.
Aggregate auf int können überlaufen.
sum(int_column) über eine Million Zeilen mit jeweils 10.000 ergibt 10^10 — das passt nicht in int. Postgres wirft Fehler. Lösung: explizit sum(col::bigint) oder die Spalte gleich als bigint anlegen. Bei numeric gibt’s das Problem nicht (kein fixer Wertebereich).
Weiterführende Ressourcen
Externe Quellen
- Numeric Types – PostgreSQL Documentation
- Mathematical Functions and Operators
- What Every Computer Scientist Should Know About Floating-Point Arithmetic — IEEE-754 erklärt
- PostgreSQL Wiki: Don’t Do This — u. a. Tipps zu Numerik