Der Primary Key ist die wichtigste Constraint einer Tabelle: er garantiert, dass jede Zeile eindeutig identifizierbar ist. Postgres legt automatisch einen Unique-Index auf die PK-Spalten an und erlaubt keine NULLs. Hier alle Varianten — Single-Column, Composite, Identity-PKs — mit modernen Best Practices.

Die einfache Form

SQL PK auf einer Spalte
CREATE TABLE customers (
    id    bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    email text NOT NULL UNIQUE,
    name  text NOT NULL
);

PRIMARY KEY bedeutet implizit:

  • NOT NULL — keine Zeile darf NULL in dieser Spalte haben
  • UNIQUE — keine zwei Zeilen dürfen den selben Wert haben
  • automatischer Index — Postgres legt einen B-tree-Index auf der PK-Spalte an

Pro Tabelle gibt es genau einen Primary Key — aber beliebig viele zusätzliche Unique-Constraints.

GENERATED ALWAYS AS IDENTITY statt SERIAL

Modern (ab PG 10): IDENTITY-Spalten — SQL-Standard-konform und idiomatisch.

SQL
-- empfohlen
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY

-- alt, immer noch erlaubt aber weniger sauber
id bigserial PRIMARY KEY

Vorteile von IDENTITY:

  • klarer SQL-Standard
  • OVERRIDING SYSTEM VALUE schützt vor versehentlichem Setzen
  • Sequence ist Owned-By der Spalte (auto-Cleanup bei DROP TABLE)
  • saubere Trennung von Werttyp (bigint) und Sequence-Logik

Mehr Details im Artikel SERIAL vs. IDENTITY.

Composite Primary Key

Manche Tabellen brauchen einen zusammengesetzten PK — typisch Junction-Tables für Many-to-Many:

SQL PK über mehrere Spalten
CREATE TABLE order_items (
    order_id   bigint NOT NULL REFERENCES orders(id),
    product_id bigint NOT NULL REFERENCES products(id),
    quantity   int    NOT NULL CHECK (quantity > 0),
    unit_price numeric(10,2) NOT NULL,
    PRIMARY KEY (order_id, product_id)
);

PRIMARY KEY (order_id, product_id) — die Kombination muss eindeutig sein. Eine einzelne order_id darf mehrfach vorkommen, eine einzelne product_id auch — aber ein Paar (order_id=42, product_id=7) nur einmal.

Reihenfolge der Spalten ist wichtig: sie bestimmt, wie der zugrundeliegende Index sortiert ist. Faustregel: zuerst die Spalte, nach der häufiger gefiltert wird.

Natural Key vs. Surrogate Key

Natural KeySurrogate Key
WertGeschäftlich (Email, ISBN, SKU)Technisch (Auto-ID, UUID)
VorteileLesbar, oft schon vorhandenStabil, kompakt, schnell
NachteileKann sich ändern, oft längerNicht aussagekräftig
EmpfehlungEher als UNIQUE-ConstraintAls PK

Pragmatischer Konsens: Surrogate Key als PK + Natural Key als zusätzliche UNIQUE-Constraint.

SQL Beides kombinieren
CREATE TABLE products (
    id   bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    sku  text   NOT NULL UNIQUE,
    name text   NOT NULL,
    ...
);

id ist stabil und kompakt — alle Foreign Keys verweisen darauf. sku ist die geschäftliche Kennung, kann sich theoretisch mal ändern (etwa bei Umstellung des Nummernsystems), ohne dass alle FKs nachgezogen werden müssen.

UUID als Primary Key

Manchmal sinnvoll: UUID statt sequenzieller Identity.

SQL
-- PG 18+ mit eingebauter UUIDv7-Funktion
CREATE TABLE events (
    id         uuid PRIMARY KEY DEFAULT uuidv7(),
    event_type text NOT NULL,
    created_at timestamptz NOT NULL DEFAULT now()
);

Vorteile von UUID-PKs:

  • Client kann ID generieren, ohne erst INSERT zu machen
  • IDs aus verschiedenen Instanzen kollidieren nicht (kein zentraler Counter nötig)
  • Schwer zu erraten — gut für public-facing IDs

Nachteile:

  • 16 Byte statt 8 Byte → mehr Speicher pro Index-Eintrag
  • UUIDv4 (random) führt zu Index-Fragmentierung — UUIDv7 (zeitsortiert) ist deutlich besser

Mehr im Artikel UUID.

PK später hinzufügen

SQL ALTER TABLE … ADD PRIMARY KEY
-- nachträglich
ALTER TABLE legacy_data
ADD PRIMARY KEY (id);

Vorsicht bei großen Tabellen: das Anlegen des Indexes blockt Schreib-Zugriffe. Sicherer Pfad:

SQL Ohne Lock — der zweistufige Weg
-- 1. Unique-Index ohne Tabelle zu sperren
CREATE UNIQUE INDEX CONCURRENTLY legacy_data_pk_idx
ON legacy_data (id);

-- 2. Existing Index als Primary Key ausweisen
ALTER TABLE legacy_data
ADD CONSTRAINT legacy_data_pkey
PRIMARY KEY USING INDEX legacy_data_pk_idx;

CREATE INDEX CONCURRENTLY blockt nur kurze Locks, dann ist der Constraint per USING INDEX schnell drangehängt.

Interessantes

bigint statt int als Default-PK.

Eine Tabelle, die mal eine Million Zeilen hatte, kann durch wachsende Workloads über 2 Milliarden hinausgehen — und int läuft bei 2,1 Milliarden über. bigint (8 Byte) ist heute der pragmatische Default; der Speicher-Unterschied ist meist irrelevant.

PK = Cluster-Order? Nein, nicht in Postgres.

Anders als InnoDB/MySQL ordnet Postgres die Tabelle NICHT physikalisch nach dem PK. Wer das will, nutzt CLUSTER tabelle USING tabelle_pkey — aber das ist ein einmaliger Sortier-Vorgang, nicht persistent. Üblicherweise nicht nötig.

Composite PKs sind okay — auch in Junction-Tables.

Manche Teams setzen reflexartig Surrogate-IDs auf jede Tabelle. Bei reinen Junction-Tables (order_items, tag_relations) ist ein Composite-PK aus den FK-Spalten oft eleganter — kein toter Surrogate, ein Zeilen-Eintrag pro Beziehung.

PK darf nachträglich geändert werden — vorsichtig.

ALTER TABLE … DROP CONSTRAINT pkey, ADD PRIMARY KEY (...) ist möglich. Bei Tabellen mit FK-Verweisen werden die natürlich ungültig. Schema-Migrationen sollten das in mehreren Schritten machen, nicht in einer Transaktion.

Foreign Keys verweisen typischerweise auf den PK.

Sie können theoretisch auf jede UNIQUE-Spalte zeigen, der PK ist aber der Default-Referenz-Punkt. Mehr im Artikel Foreign Key.

GENERATED ALWAYS oder BY DEFAULT?

ALWAYS: Wert kann nur mit OVERRIDING SYSTEM VALUE manuell gesetzt werden — verhindert Versehen. BY DEFAULT: jeder INSERT mit explizitem Wert geht durch. Für saubere Apps ALWAYS bevorzugen.

Weiterführende Ressourcen

Externe Quellen

/ Weiter

Zurück zu Constraints & Schema-Design

Zur Übersicht