Maschinennative Datentypen & Relationale Constraints: Entwurfsmuster für PostgreSQL & MariaDB
Erstellt: 08.11.2026 · Zuletzt aktualisiert: 08.11.2026 · Nejat Hakan
Management Summary: Die Wahl falscher Spaltendatentypen bläht den Speicherbedarf auf NVMe-Laufwerken auf, verstopft den CPU-Cache, führt zu schleichenden Rundungsfehlern bei Finanzberechnungen und verursacht massive Indexfragmentierung. Ein Datenbankschema ist kein passiver Speichercontainer, sondern die oberste mathematische Instanz zur Durchsetzung von Geschäftsregeln. Dieser Leitfaden beschreibt die Auswahl maschinennativer Datentypen (wie
BIGINT IDENTITY, zeitlich sortierbareUUIDv7, Festkomma-NUMERIC, UTC-konformesTIMESTAMPTZund Netzwerk-INET) sowie die deklarative Konfiguration robuster Integritätsbedingungen (CHECK,FOREIGN KEYund GiST-basierteEXCLUDE-Constraints) direkt auf Ebene der Speicher-Engine.
-
Identity & Sequenzielle IDs
Veraltete
SERIAL-Pseudotypen durch standardkonformeBIGINT GENERATED ALWAYS AS IDENTITYoderUUIDv7ersetzen – Schluss mit 32-Bit-Überläufen und B-Tree-Bloat. -
Finanzmathematik: Niemals FLOAT!
Binäre Fließkomma-Rundungsfehler eliminieren: Atomare Cent-/Satoshi-Einheiten in
BIGINTspeichern oder auf exakteNUMERIC-Typen setzen. -
Strikte UTC-Zeitstempel
Fehler durch Sommer-/Winterzeit-Umstellungen (DST) verhindern:
TIMESTAMPTZerzwingen und alle Zeitstempel auf Speicherebene in UTC normalisieren. -
Deklarative Constraints
Geschäftsinvarianten hardwarenah auf Dateiebene absichern:
CHECK-Bedingungen, verzögerte Fremdschlüssel und GiST-Exclusion-Regeln einrichten.
1. Datentyp-Matrix & Speichereffizienz
Die Wahl der Datentypen bestimmt maßgeblich, wie viele Datensätze in eine 8 KiB PostgreSQL-Page oder eine 16 KiB MariaDB-InnoDB-Page passen. Mehr Zeilen pro Speicherseite bedeuten weniger I/O-Lesevorgänge von NVMe-Datenträgern und eine dramatisch höhere Cache-Hit-Rate im Arbeitsspeicher.
| Datenbereich | ❌ Anti-Pattern | ✅ PostgreSQL (Produktiv) | ✅ MariaDB (Produktiv) | Architektonische Begründung |
|---|---|---|---|---|
| Primärschlüssel | SERIAL (32-Bit) oder zufälliges VARCHAR(36) UUIDv4 |
BIGINT GENERATED ALWAYS AS IDENTITY oder UUID (UUIDv7) |
BIGINT UNSIGNED AUTO_INCREMENT |
32-Bit-Integers laufen bei 2,14 × 109 Zeilen über; unstrukturierte UUIDv4 fragmentieren B-Tree-Blattseiten massiv. |
| Währungen & Finanzen | FLOAT / REAL / DOUBLE PRECISION |
BIGINT (Cent-Beträge) oder NUMERIC(14, 2) |
BIGINT (Cent) oder DECIMAL(14, 2) |
IEEE-754 Fließkommazahlen können Dezimalbrüche wie 0,10 + 0,20 nicht exakt binär abbilden. |
| Zeitstempel | TIMESTAMP (ohne Zeitzone) oder VARCHAR |
TIMESTAMPTZ (TIMESTAMP WITH TIME ZONE) |
DATETIME(6) oder TIMESTAMP(6) (in UTC) |
Zeitzonenlose Zeitstempel führen zu Fehlberechnungen bei Zeitumstellungen und Multi-Region-Clustern. |
| Zeichenketten | CHAR(255) (füllt Leerzeichen auf) |
TEXT oder VARCHAR(n) (in PG speicheridentisch) |
VARCHAR(n) |
CHAR(n) verschwendet Speicherplatz und fügt unnötige Leerzeichen an. |
| IP-Adressen | VARCHAR(45) |
INET / CIDR (7 oder 19 Bytes) |
INET6 (nativer 16-Byte Binärtyp) |
Syntaxprüfung bereits beim Parsen; unterstützt native binäre Subnetz-Operatoren (<<, >>). |
| Semi-strukturierte Daten | TEXT mit serialisierten JSON-Strings |
JSONB (dekomprimiertes Binärformat) |
JSON (mit nativer Syntaxvalidierung) |
JSONB ermöglicht Indexierung via GIN, punktuelle Updates und blitzschnelle Key-Lookups. |
%%{init: {'flowchart': {'wrappingWidth': 350}}}%%
flowchart TB
ROW["Logisches Datensatz-Tupel<br/>(Eingehender INSERT-Payload)"]
PK_TYPE["1. PK: BIGINT Identity<br/>(8 Bytes, Sequenz)"]
MONEY_TYPE["2. Währung: BIGINT Cents<br/>(8 Bytes, Exakte Arithmetik)"]
TIME_TYPE["3. Zeitstempel: TIMESTAMPTZ<br/>(8 Bytes, UTC-Epoche)"]
IP_TYPE["4. Netzwerk: INET<br/>(7 / 19 Bytes Binär)"]
PAGE["Ziel: 8 KiB Buffer-Page<br/>(Kompakt, schnelle Scans)"]
ROW --> PK_TYPE
PK_TYPE --> MONEY_TYPE
MONEY_TYPE --> TIME_TYPE
TIME_TYPE --> IP_TYPE
IP_TYPE --> PAGE
2. Kritische Anti-Patterns im Detail
Anti-Pattern 1: Die Fließkomma-Finanzkatastrophe
Fließkommadatentypen (FLOAT, DOUBLE PRECISION, REAL) basieren auf dem Standard IEEE-754, der Zahlen im Binärsystem darstellt. Wie der Bruch 1/3 im Dezimalsystem nicht mit endlich vielen Nachkommastellen darstellbar ist, lassen sich Zahlen wie 0,1 oder 0,2 im Binärsystem nicht exakt abbilden.
-- Den IEEE-754 Rundungsfehler in PostgreSQL nachvollziehen:
SELECT (0.1::float8 + 0.2::float8) = 0.3::float8 AS is_equal;
-- Ergibt: FALSE (0.1 + 0.2 ergibt 0.30000000000000004)
-- Saubere Lösung: Exakter Festkommatyp NUMERIC
SELECT (0.1::numeric + 0.2::numeric) = 0.3::numeric AS is_equal;
-- Ergibt: TRUE
Geldbeträge immer in Cents speichern
In durchsatzstarken Bezahlsystemen empfiehlt es sich, Währungsbeträge als ganzzahlige Anzahl der kleinsten atomaren Einheit (z. B. Euro-Cent oder Bitcoin-Satoshis) in einem BIGINT abzulegen. Dies schließt Rundungsverluste komplett aus und nutzt die native 64-Bit-Ganzzahlarithmetik der CPU.
Anti-Pattern 2: Unstrukturierte UUIDv4 vs. Zeitlich sortierte UUIDv7
Die Verwendung von rein zufälligen UUIDv4-Schlüsseln in Microservices verhindert zwar ID-Kollisionen ohne zentrale Koordination, führt in relationalen Datenbanken jedoch zu verheerender B-Tree-Index-Fragmentierung: Da neue Schlüssel völlig unvorhersehbar sind, landet jeder neue Datensatz auf einer zufälligen Blattseite (Leaf Page) auf der NVMe-SSD. Die Folge sind permanente Page-Splits und Cache-Verdrängungen.
UUIDv7 löst dieses Problem elegant, indem die führenden 48 Bits einen Unix-Zeitstempel in Millisekunden enthalten: - Sequenzielle Speicherung: Neue Einträge werden fortlaufend an das Ende der B-Tree-Blattseiten angehängt. - Cache-Lokalität: Kürzlich erstellte Datensätze liegen auf denselben Speicherseiten im RAM. - Kollisionssicherheit: 74 verbleibende Zufallsbits garantieren weltweite Eindeutigkeit.
3. Relationale Integritätsregeln in der Praxis
Ein professionelles Schema nutzt deklarative Constraints, um zu verhindern, dass fehlerhafte Daten überhaupt auf den Datenträger geschrieben werden:
%%{init: {'flowchart': {'wrappingWidth': 350}}}%%
flowchart TB
OP["Schreiboperation<br/>(INSERT / UPDATE)"]
C_NULL{"NOT NULL Prüfung"}
C_DOM{"Domänen- & Typ-Check"}
C_CHK{"CHECK-Bedingungen"}
C_FK{"Fremdschlüssel-Check"}
C_EXCL{"Unique & EXCLUDE Test"}
WRITE_OK["Commit auf Heap-Page"]
WRITE_ERR["Abbruch mit SQL-Fehler"]
OP --> C_NULL
C_NULL -->|OK| C_DOM
C_NULL -->|Fehler| WRITE_ERR
C_DOM -->|OK| C_CHK
C_DOM -->|Fehler| WRITE_ERR
C_CHK -->|OK| C_FK
C_CHK -->|Fehler| WRITE_ERR
C_FK -->|OK| C_EXCL
C_FK -->|Fehler| WRITE_ERR
C_EXCL -->|OK| WRITE_OK
C_EXCL -->|Fehler| WRITE_ERR
Typen von Constraints & Best Practices
NOT NULL: Schließt die fehleranfällige dreiwertige SQL-Logik (TRUE,FALSE,UNKNOWN) aus. Jede Spalte sollte standardmäßigNOT NULLsein, es sei denn, ein fehlender Wert hat eine explizite fachliche Bedeutung.CHECK-Bedingungen: Mathematische und logische Grenzen definieren (z. B.CHECK (stock_qty >= 0)oderCHECK (status IN ('PENDING', 'PAID', 'SHIPPED'))).FOREIGN KEY-Regeln:ON DELETE RESTRICT(Standard): Verhindert das Löschen eines Elterndatensatzes, solange abhängige Kinddaten existieren.ON DELETE CASCADE: Löscht abhängige Kinddaten automatisch mit (mit Bedacht einsetzen).DEFERRABLE INITIALLY DEFERRED: Prüft die Gültigkeit des Fremdschlüssels erst beimCOMMIT, wodurch zyklische Verweise innerhalb einer Transaktion möglich werden.
EXCLUDE-Constraints (PostgreSQL): Nutzt GiST-Indizes, um überlappende Zeitbereiche oder Raumbuchungen ohne Race Conditions auf Datenbankebene zu sperren.
4. Hands-on-Labor: Gehärtetes Produktivschema
Führe dieses Skript in PostgreSQL 17 aus, um ein relational gehärtetes Schema mit Identity-Spalten, Check-Constraints, IP-Netzwerktypen und Exclusion-Regeln anzulegen:
-- Datei: /opt/db-ops/hardened_schema_workshop.sql
BEGIN;
-- btree_gist Erweiterung für Range-Exclusion-Regeln aktivieren
CREATE EXTENSION IF NOT EXISTS btree_gist;
-- 1. Kunden-Relation
CREATE TABLE customers (
customer_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
email TEXT NOT NULL UNIQUE
CHECK (email ~* '^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$'),
company_name VARCHAR(128) NOT NULL,
ipv4_registered INET NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT clock_timestamp()
);
-- 2. Produkt-Relation
CREATE TABLE products (
product_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
sku VARCHAR(32) NOT NULL UNIQUE,
title TEXT NOT NULL,
price_cents BIGINT NOT NULL CHECK (price_cents >= 0),
stock_qty INT NOT NULL DEFAULT 0 CHECK (stock_qty >= 0),
metadata JSONB NOT NULL DEFAULT '{}'::jsonb
);
-- 3. Bestellungs-Relation
CREATE TABLE orders (
order_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
customer_id BIGINT NOT NULL REFERENCES customers(customer_id) ON DELETE RESTRICT,
status VARCHAR(16) NOT NULL DEFAULT 'PENDING'
CHECK (status IN ('PENDING', 'PAID', 'SHIPPED', 'CANCELLED')),
ordered_at TIMESTAMPTZ NOT NULL DEFAULT clock_timestamp()
);
-- 4. Bestellpositionen (Zusammengesetzter Primärschlüssel)
CREATE TABLE order_items (
order_id BIGINT NOT NULL REFERENCES orders(order_id) ON DELETE CASCADE,
product_id BIGINT NOT NULL REFERENCES products(product_id) ON DELETE RESTRICT,
quantity INT NOT NULL CHECK (quantity > 0),
unit_cents BIGINT NOT NULL CHECK (unit_cents >= 0),
PRIMARY KEY (order_id, product_id)
);
-- 5. Server-Reservierungen mit GiST-Exclusion (Verhindert Doppelbuchungen zuverlässig)
CREATE TABLE server_reservations (
reservation_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
server_node TEXT NOT NULL,
reserved_during TSRANGE NOT NULL,
EXCLUDE USING gist (server_node WITH =, reserved_during WITH &&)
);
COMMIT;