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
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.
-- empfohlen
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY
-- alt, immer noch erlaubt aber weniger sauber
id bigserial PRIMARY KEYVorteile von IDENTITY:
- klarer SQL-Standard
OVERRIDING SYSTEM VALUEschü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:
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 Key | Surrogate Key | |
|---|---|---|
| Wert | Geschäftlich (Email, ISBN, SKU) | Technisch (Auto-ID, UUID) |
| Vorteile | Lesbar, oft schon vorhanden | Stabil, kompakt, schnell |
| Nachteile | Kann sich ändern, oft länger | Nicht aussagekräftig |
| Empfehlung | Eher als UNIQUE-Constraint | Als PK |
Pragmatischer Konsens: Surrogate Key als PK + Natural Key als zusätzliche UNIQUE-Constraint.
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.
-- 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
-- nachträglich
ALTER TABLE legacy_data
ADD PRIMARY KEY (id);Vorsicht bei großen Tabellen: das Anlegen des Indexes blockt Schreib-Zugriffe. Sicherer Pfad:
-- 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.