Zum Inhalt

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 sortierbare UUIDv7, Festkomma-NUMERIC, UTC-konformes TIMESTAMPTZ und Netzwerk-INET) sowie die deklarative Konfiguration robuster Integritätsbedingungen (CHECK, FOREIGN KEY und GiST-basierte EXCLUDE-Constraints) direkt auf Ebene der Speicher-Engine.

  • Identity & Sequenzielle IDs


    Veraltete SERIAL-Pseudotypen durch standardkonforme BIGINT GENERATED ALWAYS AS IDENTITY oder UUIDv7 ersetzen – Schluss mit 32-Bit-Überläufen und B-Tree-Bloat.

  • Finanzmathematik: Niemals FLOAT!


    Binäre Fließkomma-Rundungsfehler eliminieren: Atomare Cent-/Satoshi-Einheiten in BIGINT speichern oder auf exakte NUMERIC-Typen setzen.

  • Strikte UTC-Zeitstempel


    Fehler durch Sommer-/Winterzeit-Umstellungen (DST) verhindern: TIMESTAMPTZ erzwingen 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

  1. NOT NULL: Schließt die fehleranfällige dreiwertige SQL-Logik (TRUE, FALSE, UNKNOWN) aus. Jede Spalte sollte standardmäßig NOT NULL sein, es sei denn, ein fehlender Wert hat eine explizite fachliche Bedeutung.
  2. CHECK-Bedingungen: Mathematische und logische Grenzen definieren (z. B. CHECK (stock_qty >= 0) oder CHECK (status IN ('PENDING', 'PAID', 'SHIPPED'))).
  3. 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 beim COMMIT, wodurch zyklische Verweise innerhalb einer Transaktion möglich werden.
  4. 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;