Ein enum ist eine vom Schema definierte Liste fester Werte — perfekt für Status-Felder, Kategorien oder Berechtigungs-Stufen. PostgreSQL unterstützt das nativ, aber: Enums haben einige unangenehme Eigenschaften, die du kennen solltest, bevor du dich darauf festlegst. Oft ist eine Lookup-Tabelle mit text und Foreign Key flexibler.

Enum erstellen und verwenden

SQL Status-Enum für Bestellungen
-- 1. Den Enum-Typ definieren (datenbankweit)
CREATE TYPE order_status AS ENUM (
    'pending',
    'paid',
    'shipped',
    'delivered',
    'cancelled'
);

-- 2. In einer Tabelle verwenden
CREATE TABLE orders (
    id     bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    total  numeric(10, 2) NOT NULL,
    status order_status NOT NULL DEFAULT 'pending'
);

Der Enum-Typ ist ein eigenständiges Datenbankobjekt — du definierst ihn einmal und kannst ihn in vielen Tabellen verwenden.

SQL Insert und Update
myapp=> INSERT INTO orders (total) VALUES (99.95) RETURNING id, status;
 id | status
----+---------
  1 | pending

myapp=> UPDATE orders SET status = 'paid' WHERE id = 1;
UPDATE 1

myapp=> UPDATE orders SET status = 'invalid_status' WHERE id = 1;
ERROR:  invalid input value for enum order_status: "invalid_status"

Postgres lehnt Werte ab, die nicht im Enum definiert sind — das ist die Hauptstärke gegenüber einem normalen text-Feld.

Sortierung und Vergleich

Enum-Werte werden in der Reihenfolge der Definition sortiert, nicht alphabetisch:

SQL
myapp=> SELECT status FROM orders ORDER BY status;
  status
-----------
 pending
 paid
 shipped
 delivered
 cancelled

Das ist einer der nettesten Aspekte: bei Status-Lifecycles ist die Reihenfolge oft natürlich (pending vor paid vor shipped), und das spiegelt sich automatisch in ORDER BY wider.

Vergleiche <, > funktionieren entsprechend:

SQL
SELECT * FROM orders WHERE status >= 'paid';
-- Liefert: paid, shipped, delivered, cancelled

Werte hinzufügen — und warum’s heikel ist

Neuen Wert ergänzen:

SQL
ALTER TYPE order_status ADD VALUE 'refunded';

-- Mit Position relativ zu existierenden Werten:
ALTER TYPE order_status ADD VALUE 'reviewing' BEFORE 'paid';
ALTER TYPE order_status ADD VALUE 'returned' AFTER 'delivered';

Klingt einfach. Hat aber Einschränkungen:

  • ALTER TYPE ADD VALUE kann nicht in einer Transaktion mit anderen DDL-Statements kombiniert werden (außer BEFORE / AFTER-Varianten in PG 12+).
  • In Migrations-Tools wie Liquibase oder Flyway, die alles in eine Transaktion packen, ist das ein Stolperstein.

Beispiel-Fehler:

SQL
myapp=> BEGIN;
myapp=> ALTER TYPE order_status ADD VALUE 'refunded';
myapp=> -- Versuche, den neuen Wert zu nutzen:
myapp=> SELECT 'refunded'::order_status;
ERROR:  unsafe use of new value "refunded" of enum type order_status
HINT:   New enum values must be committed before they can be used.

Lösung: ALTER TYPE … ADD VALUE separat committen, dann den neuen Wert verwenden.

Werte entfernen — geht NICHT direkt

Postgres bietet keinen direkten Weg, einen Wert aus einem Enum zu löschen:

SQL
myapp=> ALTER TYPE order_status DROP VALUE 'refunded';
ERROR:  syntax error at or near "DROP"

Das ist Absicht — wenn der Wert irgendwo in der Datenbank vorkommt, würde sein Verschwinden kaputte Zeilen hinterlassen. Stattdessen muss man:

  1. Alle Zeilen mit dem zu entfernenden Wert auf einen anderen umstellen (UPDATE).
  2. Einen neuen Enum-Typ ohne den Wert erstellen.
  3. Alle Spalten auf den neuen Typ casten.
  4. Den alten Typ droppen.
SQL Wert aus Enum entfernen — der vollständige Tanz
BEGIN;

-- 1. Bestehende Zeilen umsetzen
UPDATE orders SET status = 'cancelled' WHERE status = 'refunded';

-- 2. Neuen Typ ohne 'refunded'
CREATE TYPE order_status_new AS ENUM (
    'pending', 'paid', 'shipped', 'delivered', 'cancelled'
);

-- 3. Spalten casten
ALTER TABLE orders
    ALTER COLUMN status TYPE order_status_new
    USING status::text::order_status_new;

-- 4. Default neu setzen, alten Typ droppen, neuen umbenennen
ALTER TABLE orders ALTER COLUMN status SET DEFAULT 'pending';
DROP TYPE order_status;
ALTER TYPE order_status_new RENAME TO order_status;

COMMIT;

Das ist eine ausgewachsene Migration — und der Hauptgrund, warum viele Teams ab einer gewissen Tabellengrößer von Enum auf Lookup-Tabelle umsteigen.

Werte umbenennen

Glücklicherweise einfach:

SQL
ALTER TYPE order_status RENAME VALUE 'shipped' TO 'in_transit';

Existierende Zeilen, die shipped enthielten, zeigen jetzt in_transit — automatisch, ohne dass die Zeilen umgeschrieben werden müssen. Postgres speichert intern eine OID pro Enum-Wert; nur das Label ändert sich.

Alternative: Lookup-Tabelle

SQL Statt Enum: separate Status-Tabelle
CREATE TABLE order_statuses (
    code        text PRIMARY KEY,
    label_de    text NOT NULL,
    label_en    text NOT NULL,
    sort_order  integer NOT NULL,
    is_terminal boolean NOT NULL DEFAULT FALSE
);

INSERT INTO order_statuses (code, label_de, label_en, sort_order, is_terminal) VALUES
    ('pending',   'Ausstehend',  'Pending',   10, FALSE),
    ('paid',      'Bezahlt',     'Paid',      20, FALSE),
    ('shipped',   'Versandt',    'Shipped',   30, FALSE),
    ('delivered', 'Geliefert',   'Delivered', 40, TRUE),
    ('cancelled', 'Storniert',   'Cancelled', 99, TRUE);

CREATE TABLE orders (
    id     bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    total  numeric(10, 2) NOT NULL,
    status text NOT NULL REFERENCES order_statuses(code) DEFAULT 'pending'
);

Vorteile:

  • Werte hinzufügen/entfernen über normale INSERT/DELETE — keine DDL-Akrobatik.
  • Zusätzliche Metadaten (Übersetzungen, Sortierung, Status-Klassen) direkt mit am Wert.
  • Foreign Key garantiert Konsistenz.

Nachteile:

  • Joins für die Übersetzung (SELECT o.*, s.label_de FROM orders o JOIN order_statuses s ON …).
  • Etwas mehr Speicher (text-Werte statt 4-Byte-OIDs).

In der Praxis: für wenige (3-5), stabile Werte ist Enum okay. Für viele Werte oder häufige Änderungen ist die Lookup-Tabelle die bessere Wahl.

Enum inspizieren

SQL
-- Alle Werte eines Enums in Reihenfolge
myapp=> SELECT enum_range(NULL::order_status);
         enum_range
---------------------------
 {pending,paid,shipped,delivered,cancelled}

-- Erster und letzter Wert
myapp=> SELECT enum_first(NULL::order_status), enum_last(NULL::order_status);
 enum_first | enum_last
------------+-----------
 pending    | cancelled

-- Alle Enums in der Datenbank
myapp=> \dT+ *

\dT (mit + für Details) zeigt alle benutzerdefinierten Typen, inklusive Enums.

Häufige Stolperfallen

ALTER TYPE ADD VALUE in einer Transaktion ist tückisch.

Innerhalb einer Transaktion kann der neu hinzugefügte Wert NICHT verwendet werden — Postgres weigert sich. Migrations-Tools, die alles in eine Transaktion packen, müssen ALTER TYPE ADD VALUE in einem separaten Schritt vor der eigentlichen Migration ausführen. Workaround: zwei Migrations-Files, eines für ADD VALUE, das nächste für die Verwendung.

Werte entfernen erfordert kompletten Typ-Tausch.

Es gibt kein ALTER TYPE … DROP VALUE. Der einzige Weg ist neuer Typ + Spalten umkasten + alten Typ droppen — eine teure Operation bei großen Tabellen, weil ALTER COLUMN TYPE die Spalte rewrited (außer in seltenen kompatiblen Fällen). Lookup-Tabelle vermeidet das komplett.

Enums in mehreren Schemas duplizieren oft.

Wenn du ein Multi-Tenant-Setup mit Schema-pro-Tenant hast, brauchst du den Enum-Typ in jedem Schema. Lookup-Tabellen leben in einem zentralen Schema und werden referenziert. Enum: Pflege-Mehraufwand pro Tenant.

Sortier-Reihenfolge ist die Definition-Reihenfolge.

Praktisch für Lifecycle-Status (pending < paid < shipped), aber unflexibel: wenn du später einen Status „pre_paid” zwischen pending und paid einfügen willst, kannst du das mit ADD VALUE BEFORE 'paid' lösen — aber bei komplexen Reorderings oft schmerzhaft.

Cast zu text ist deine Brücke.

Wenn du Werte aus einem Enum in einem anderen Kontext brauchst (z. B. Vergleich mit text), gibt’s 'status'::text-Cast. Ebenso umgekehrt: 'paid'::order_status. Das ist auch der Weg, beim Migrieren zwischen alten und neuen Enum-Typen.

Enums in JSON-APIs als String serialisieren.

Treiber liefern Enum-Werte als String — wie text. JavaScript/Python sehen das einfach als String mit fester Wertemenge. Wer in TypeScript Type-Safety will: einen TS-Enum aus den DB-Werten generieren (mit Tools wie pg-enum-types).

Weiterführende Ressourcen

Externe Quellen

/ Weiter

Zurück zu Datentypen

Zur Übersicht