Relational Database

PostgreSQL

PostgreSQL ist die relationale Datenbank, die Korrektheit ernst nimmt. Starke Typen, echte Transaktionen, JSONB und ein Erweiterungssystem machen sie zur Standardwahl für neue Anwendungen.

intermediate16 min readUpdated 16. Sept. 2026
schema.sql
sql
-- schema.sql
CREATE TABLE orders (
  id           bigserial PRIMARY KEY,
  user_id      bigint NOT NULL REFERENCES users (id),
  status       text NOT NULL DEFAULT 'pending',
  total_cents  integer NOT NULL CHECK (total_cents >= 0),
  created_at   timestamptz NOT NULL DEFAULT now(),
  metadata     jsonb NOT NULL DEFAULT '{}'::jsonb
);

CREATE INDEX orders_user_created_idx
  ON orders (user_id, created_at DESC);
Veröffentlicht
1989
Lizenz
PostgreSQL License (open source)
Modell
Relational + Dokument (JSONB)
Standard-Isolation
Read Committed
Storage Engine
Heap mit MVCC
Aktuellste Major-Version
17

Warum es wichtig ist

Warum Postgres weiterhin gewinnt

Standards und Korrektheit

Postgres folgt eng dem SQL-Standard, erzwingt Constraints direkt in der Engine und betrachtet Datenintegrität als nicht verhandelbar und nicht bloß als Konvention.

Ein Ökosystem aus Extensions

PostGIS für Geodaten, pgvector für Embeddings, pg_stat_statements für Query-Einblicke. Der Kern bleibt schlank, während Extensions ganze Domänen hinzufügen.

JSONB, wenn man es braucht

Ein binärer JSON-Typ mit Indexierung und Operatoren bedeutet, dass dokumentenbasierte Daten neben relationalen Daten existieren können, ohne eine zweite Datenbank zu benötigen.

Das Gesamtbild

Die drei Ideen hinter Postgres

Ein typisiertes relationales Modell, Transaktionen, die niemals lügen, und ein Erweiterungssystem, das es der Datenbank ermöglicht, mit dir zu wachsen.

Typisierte Relationen

Modell

Tabellen, Spalten und Constraints beschreiben die Form deiner Daten, und die Engine weigert sich, alles zu speichern, was die Regeln bricht.

MVCC-Transaktionen

Isolierung

Reader blockieren niemals Writer. Jede Transaktion sieht einen konsistenten Snapshot mit Isolationsstufen, die logisch nachvollziehbar sind.

Gepoolte Verbindungen

Skalierung

Postgres forked einen Prozess pro Verbindung, daher sitzt ein Pooler wie PgBouncer davor, um Tausende von Clients effizient zu verwalten.

HTML5 auf einen Blick

Was im Paket enthalten ist

Tabellen und Constraints

PRIMARY KEY, FOREIGN KEY, UNIQUE, CHECK und NOT NULL erzwingen Regeln dort, wo die Daten liegen.

Vielfältige Indexe

B-tree, GIN, GiST, BRIN und Hash-Indexe decken Gleichheit, Bereiche, Volltext und JSON ab.

EXPLAIN ANALYZE

Sieh den tatsächlichen Plan und die Timings für jede Query, bevor du blind versuchst, sie zu optimieren.

MVCC

Multi-Version Concurrency Control gibt jeder Transaktion einen stabilen Snapshot ohne Read-Locks.

JSONB

Speichere, abfrage und indexiere semi-strukturierte Dokumente neben normalen Spalten.

Extensions

CREATE EXTENSION fügt Funktionen hinzu, von der UUID-Generierung bis zur geospatialen Suche.

Datenmodell

Eine Zeile pro Bestellung

Eine Zeile repräsentiert eine einzelne Bestellung eines Benutzers, mit einem veränderbaren Status und einem JSONB-Metadaten-Container.

Die Orders-TabellePostgreSQL table
  • idbigserialSurrogat-Primärschlüssel, generiert durch eine Sequence
  • user_idbigintForeign Key zu den Benutzern; der Besitzer der Bestellung
  • statustextLifecycle-Status wie pending, paid oder shipped
  • total_centsintegerBetrag in kleinsten Einheiten, um Floating-Point-Probleme bei Geld zu vermeiden
  • created_attimestamptzZeitpunkt des Einfügens in UTC, gespeichert mit Zeitzone
  • metadatajsonbOptionale Extras wie Gutscheincodes oder Geräteinformationen

Eine Zeile repräsentiert eine einzelne Bestellung eines Benutzers, mit einem veränderbaren Status und einem JSONB-Metadaten-Container.

Eine kurze Geschichte

Vier Jahrzehnte Präzision

  1. 1986

    Das Berkeley POSTGRES Projekt

    Das Team von Michael Stonebraker startet ein relationales System, um Erweiterbarkeit und fortgeschrittene Typen zu erforschen.

    86
  2. 1996

    Postgres95 wird zu PostgreSQL

    Die SQL-Unterstützung wird implementiert und das Projekt wird umbenannt, was es für eine globale Community öffnet.

    96
  3. 2010

    Streaming Replication und Hot Standby

    Integrierte asynchrone Replikation macht Read-Replicas und Failover zu einem First-Class-Feature.

    10
  4. 2014

    JSONB erscheint

    Ein binärer, indexierbarer JSON-Typ macht Postgres auch zu einem ernsthaften Document Store.

    14
  5. 2020

    Generated Columns und ein wachsendes Ökosystem

    Managed Postgres und Extensions wie pgvector bringen es in den Bereich Analytics und KI-Workloads.

    20

Der vollständige Leitfaden

PostgreSQL: Alles was Sie wissen müssen

Was ist PostgreSQL?

PostgreSQL ist eine Open-Source-relationale Datenbank, die dafür bekannt ist, die grundlegenden Dinge absolut korrekt zu erledigen. Sie speichert Daten in Tabellen, erzwingt Regeln durch Constraints, kapselt Änderungen in echten Transaktionen und stellt all dies über Standard-SQL zur Verfügung. Sie wird seit den 1980er Jahren kontinuierlich weiterentwickelt und wird von einer breiten Community statt von einem einzelnen Unternehmen gesteuert.

Diese Langlebigkeit zeigt sich in den Details. Postgres besitzt das reichhaltigste Typsystem aller gängigen Datenbanken, einen Erweiterungsmechanismus, der es Drittanbietern ermöglicht, komplett neue Funktionen hinzuzufügen, und einen Query Planner, der alles bewältigt – vom einfachen Point-Lookup bis hin zu komplexen analytischen Window-Queries. In den meisten Unternehmen ist sie die Standard-relationale Datenbank für neue Anwendungen und ist fast überall als Managed Service verfügbar.

Wenn Sie sich für eine Datenbank entscheiden, die Sie tiefgreifend lernen möchten, dann ist dies diejenige, die die investierte Zeit am meisten belohnt.

Warum Teams Postgres wählen

Drei Eigenschaften erklären den Großteil seiner Popularität.

Es ist standardmäßig korrekt. Constraints werden in der Engine erzwungen, nicht im Anwendungscode. Ein CHECK kann nicht durch einen fehlerhaften Service umgangen werden, ein FOREIGN KEY kann nicht von einem Batch-Job ignoriert werden und ein UNIQUE-Index kann nicht durch zwei gleichzeitige Anfragen in einen Race-Condition-Zustand versetzt werden. Datenintegrität wird so zu einer Eigenschaft des Schemas.

Es ist erweiterbar. Anstatt jedes Feature direkt in den Core zu integrieren, bietet Postgres Hooks für neue Typen, Operatoren, Index-Methoden und prozedurale Sprachen. Auf diese Weise existieren PostGIS, pgvector, TimescaleDB und Dutzende andere Projekte als Extensions statt als Forks.

Es spricht Standard-SQL. Kenntnisse und Queries lassen sich zwischen Datenbanken, ORMs und Tools übertragen. Man muss keinen proprietären Dialekt lernen, um loszulegen.

Das relationale Modell im Überblick

Eine relationale Datenbank speichert Daten in Tabellen, die aus Zeilen und Spalten bestehen. Jede Tabelle besitzt einen Primary Key, der eine Zeile eindeutig identifiziert. Beziehungen werden über Foreign Keys ausgedrückt, die auf andere Tabellen verweisen. Ziel ist es, jeden Fakt nur einmal zu speichern und ihn bei Bedarf über Joins wieder zusammenzuführen.

Nehmen wir Benutzer und Bestellungen als Beispiel. Anstatt die E-Mail-Adresse eines Kunden bei jeder Bestellung zu wiederholen, speichern Sie die E-Mail einmal in users und referenzieren den Benutzer in orders über user_id. Dies nennt man Normalisierung. Sie verhindert die klassische Anomalie, bei der eine Zeile aktualisiert wird, während tausend andere noch den alten Wert enthalten.

Normalisierung ist keine Religion. Die dritte Normalform ist der vernünftige Standard. Eine bewusste Denormalisierung – etwa ein gecachtes order_count oder eine Materialized View – ist eine Performance-Entscheidung, die Sie später auf Basis konkreter Messwerte treffen.

Datentypen, die sich auszahlen

Postgres bietet mehr Typen an, als die meisten Projekte benötigen, aber eine Handvoll ist ständig relevant:

  • text — eine Zeichenfolge mit variabler Länge ohne willkürliches Limit. Bevorzuge diesen Typ gegenüber varchar(n), es sei denn, das Limit ist eine echte Business-Regel.
  • integer und bigint — ganze Zahlen. Nutze bigint für alles, was theoretisch unbegrenzt wachsen könnte, wie zum Beispiel IDs aus einer Sequence.
  • numeric(p, s) — exakte Dezimalarithmetik. Dies ist der richtige Typ für Geldbeträge, nicht real oder double precision.
  • timestamptz — ein in UTC gespeicherter Zeitstempel mit Zeitzonen-Unterstützung. Bevorzuge diesen Typ für Anwendungsdaten immer gegenüber timestamp.
  • uuid — ein 128-Bit-Identifikator, nützlich wenn Clients IDs generieren oder wenn du keine Zählerstände preisgeben möchtest.
  • jsonb — binäres JSON, das indexiert und abgefragt werden kann.
  • Arrays und composite types — text[], integer[] und benutzerdefinierte Row-Typen, praktisch für Tags und kleine geordnete Listen.

Das Beispiel mit dem Geld ist wichtig zu verinnerlichen: 0.1 + 0.2 ist im binären Gleitkommaformat nicht 0.3. Speichere Beträge als ganzzahlige kleinste Einheiten (total_cents) oder als numeric, niemals als float.

Tabellen und Constraints erstellen

Ein Schema ist ein Vertrag. Jeder Constraint, den Sie deklarieren, ist eine Klasse von Bugs, die Sie nicht mehr ausliefern können.

CREATE TABLE users (
  id         bigserial PRIMARY KEY,
  email      text NOT NULL UNIQUE,
  name       text NOT NULL CHECK (length(name) > 0),
  created_at timestamptz NOT NULL DEFAULT now()
);

CREATE TABLE orders (
  id          bigserial PRIMARY KEY,
  user_id     bigint NOT NULL REFERENCES users (id) ON DELETE CASCADE,
  status      text NOT NULL DEFAULT 'pending'
                CHECK (status IN ('pending', 'paid', 'shipped', 'cancelled')),
  total_cents integer NOT NULL CHECK (total_cents >= 0),
  created_at  timestamptz NOT NULL DEFAULT now(),
  metadata    jsonb NOT NULL DEFAULT '{}'::jsonb
);

Die hier getroffenen Entscheidungen sind bewusst gewählt. NOT NULL schließt eine ganze Familie von Bugs durch fehlende Daten aus. UNIQUE auf email erzwingt die Invariante direkt in der Datenbank – dem einzigen Ort, an dem eine Race Condition sie nicht aushebeln kann. Der CHECK auf status dokumentiert und erzwingt die State Machine. ON DELETE CASCADE legt fest, was passiert, wenn ein Benutzer gelöscht wird, anstatt verwaiste Datensätze zu hinterlassen.

Bevorzugen Sie es, Constraints in derselben Migration hinzuzufügen, in der die Tabelle erstellt wird. Einen NOT NULL oder einen Foreign Key nachträglich hinzuzufügen, bedeutet, dass Sie zuerst alle ungültigen Zeilen bereinigen müssen, die sich während der Zeit ohne diese Regel angesammelt haben.

Abfragen mit Joins

Die meisten realen Abfragen kombinieren Tabellen. Ein Join verknüpft Zeilen aus zwei Relationen basierend auf einer Bedingung, wobei der gewählte Join-Typ bestimmt, was mit nicht zugeordneten Zeilen passiert.

SELECT u.email,
       o.id,
       o.total_cents
FROM users u
JOIN orders o ON o.user_id = u.id
WHERE o.status = 'paid'
ORDER BY o.created_at DESC
LIMIT 50;

JOIN (oder INNER JOIN) behält nur passende Paare. LEFT JOIN behält jede Zeile auf der linken Seite und füllt die rechte Seite mit NULL auf, wenn keine Übereinstimmung vorliegt – der Standardweg, um die Frage „welche Benutzer haben keine Bestellungen“ zu beantworten:

SELECT u.email, count(o.id) AS orders
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
GROUP BY u.id, u.email
HAVING count(o.id) = 0;

Aggregate wie count, sum, avg, min und max fassen Gruppen von Zeilen zu einem einzigen Wert zusammen. Jede Spalte im SELECT, die sich nicht innerhalb eines Aggregats befindet, muss im GROUP BY aufgeführt werden. Die WHERE-Klausel filtert Zeilen vor der Gruppierung; HAVING filtert Gruppen danach. Die Verwechslung dieser beiden ist eine häufige Ursache für verwirrende Ergebnisse.

Indexe und EXPLAIN ANALYZE

Ein Index ist eine sortierte Struktur, die es dem Planner ermöglicht, Zeilen zu finden, ohne die gesamte Tabelle scannen zu müssen. Der Standard ist der B-tree, der Gleichheits- und Bereichsprädikate bedient und ORDER BY direkt unterstützt.

CREATE INDEX orders_user_created_idx
  ON orders (user_id, created_at DESC);

Ein composite index wie dieser deckt Abfragen ab, die nach user_id filtern und nach created_at sortieren. Die Reihenfolge der Spalten ist entscheidend: Die linkeste Spalte muss in der Abfrage vorkommen, damit der Index für die Filterung nützlich ist. Dies ist die Leftmost-Prefix-Regel, und genau deshalb beginnt das Index-Design bei den Abfragen und nicht bei den Spalten.

Andere Index-Typen decken unterschiedliche Anwendungsfälle ab:

  • GIN — invertierte Indexe für jsonb, Arrays und Volltextsuche (tsvector).
  • GiST — Geometrie, Bereiche und Nearest-Neighbour-Suche, wird intensiv von PostGIS genutzt.
  • BRIN — winzige Indexe über natürlich geordnete Daten, wie zum Beispiel Append-only-Timestamp-Tabellen.
  • Hash — reine Gleichheits-Lookups, selten benötigt, da B-tree diese ebenfalls handhabt.

Raten Sie niemals, ob ein Index verwendet wird. Fragen Sie den Planner:

EXPLAIN ANALYZE
SELECT *
FROM orders
WHERE user_id = 42
ORDER BY created_at DESC
LIMIT 10;

Die Ausgabe zeigt den Plan-Baum, die geschätzten Kosten und – mit ANALYZE – die tatsächlichen Zeilen und die Zeit.

Limit  (cost=0.43..12.94 rows=10 width=48)
       (actual time=0.031..0.079 rows=10 loops=1)
  ->  Index Scan Backward using orders_user_created_idx on orders
        (cost=0.43..521.10 rows=417 width=48)
        (actual time=0.029..0.070 rows=10 loops=1)
        Index Cond: (user_id = 42)
Planning Time: 0.184 ms
Execution Time: 0.108 ms

Lesen Sie das Ergebnis von innen nach außen. Hier bestätigt der Index Scan mit Index Cond: (user_id = 42), dass der composite index seine Arbeit erledigt. Achten Sie auf einen Seq Scan bei einer großen Tabelle, wo Sie einen Index Scan erwartet hätten, sowie auf eine große Lücke zwischen geschätzten und tatsächlichen Zeilen, was meist auf veraltete Statistiken hindeutet. Führen Sie ANALYZE orders; aus, um diese zu aktualisieren, und EXPLAIN (ANALYZE, BUFFERS), um zu sehen, wie viele Daten aus dem Cache im Vergleich zur Festplatte gelesen wurden.

Transaktionen und MVCC

Eine Transaktion gruppiert Statements so, dass entweder alle wirksam werden oder keine einzige. Postgres implementiert Transaktionen mittels MVCC: Anstatt Zeilen für Leser zu sperren, werden mehrere Versionen vorgehalten und jeder Transaktion wird ein Snapshot zugewiesen.

BEGIN;
UPDATE accounts SET balance_cents = balance_cents - 5000 WHERE id = 1;
UPDATE accounts SET balance_cents = balance_cents + 5000 WHERE id = 2;
COMMIT;

Falls etwas fehlschlägt, macht ROLLBACK die gesamte Transaktion rückgängig. Der Anwendungscode sollte mehrstufige Schreibvorgänge in eine Transaktion einschließen und darauf vorbereitet sein, bei Serialisierungsfehlern (serialization failures) einen erneuten Versuch zu starten, wenn striktere Isolationsstufen verwendet werden.

Die Isolationsstufen legen fest, was eine Transaktion beobachten kann:

  • Read Committed (Standard) — jedes Statement sieht einen aktuellen Snapshot; geeignet für die meisten Workloads.
  • Repeatable Read — die gesamte Transaktion sieht einen einzigen Snapshot; nützlich, wenn dieselben Zeilen wiederholt gelesen werden.
  • Serializable — Transaktionen verhalten sich so, als würden sie nacheinander ausgeführt; die stärkste Garantie und die Stufe, bei der Retries am wahrscheinlichsten sind.

MVCC hat seinen Preis: Aktualisierte und gelöschte Zeilen hinterlassen sogenannte “dead tuples”. Autovacuum gibt diese im Hintergrund wieder frei. Lang laufende Transaktionen verhindern die Bereinigung und führen zu einem Anwachsen der Tabellen (bloat), daher sollten Transaktionen kurz gehalten und das Vacuum bei Tabellen mit hoher Änderungsrate überwacht werden.

Connection Pooling mit PgBouncer

Postgres verwaltet jede Client-Verbindung mit einem eigenen Prozess des Betriebssystems. Das ist zwar robust, bedeutet aber auch, dass Verbindungen ressourcenintensiver sind als in MySQL und bereits einige hundert inaktive Clients einen erheblichen Teil des Arbeitsspeichers beanspruchen können. Das praktische Limit liegt bei max_connections; wird dieses überschritten, führt dies zu Fehlern statt zu einer kontrollierten Leistungsreduzierung.

Die Lösung ist ein Connection Pooler. PgBouncer sitzt zwischen der Anwendung und Postgres, hält einen kleinen Pool an echten Verbindungen vor und multiplexed viele Client-Verbindungen auf diese. Es bietet drei Modi:

  • Session pooling — eine Server-Verbindung wird für die gesamte Session des Clients vorgehalten.
  • Transaction pooling — eine Server-Verbindung wird nur für die Dauer einer Transaktion vorgehalten; die gängigste Wahl für Web-Apps.
  • Statement pooling — eine Verbindung wird nur für ein einzelnes Statement vorgehalten; dies ist die aggressivste und restriktivste Variante.

Transaction Pooling deaktiviert Features mit Session-Scope, wie zum Beispiel SET, advisory locks über mehrere Statements hinweg und LISTEN. Verwenden Sie SET LOCAL innerhalb von Transaktionen und stellen Sie sicher, dass Ihr ORM im Transaction Mode korrekt funktioniert.

Unabhängig von Ihrer Wahl sollten Sie auch auf der Anwendungsseite einen Pool verwenden. Ein Pool pro Prozess mit einem sinnvollen Maximum, ergänzt durch einen Pooler davor, ist das Standard-Setup. Öffnen Sie niemals eine neue Verbindung pro Request.

JSONB: Wann man es einsetzen sollte

jsonb speichert JSON in einer zerlegten Binärform, die Indexierung und eine Vielzahl von Operatoren unterstützt. Es ist äußerst nützlich, wird aber auch oft überstrapaziert.

SELECT id, metadata->>'coupon' AS coupon
FROM orders
WHERE metadata @> '{"channel": "mobile"}';

Der @> Containment-Operator kann einen GIN-Index nutzen:

CREATE INDEX orders_metadata_idx ON orders USING gin (metadata);

Nutzen Sie JSONB für Daten, deren Struktur tatsächlich variabel ist: Webhook-Payloads, Antworten von Drittanbietern, benutzerdefinierte Attribute oder Feature-Flags. Nutzen Sie es nicht, nur um sich das Schreiben einer Migration zu ersparen. Felder, nach denen Sie filtern, die Sie joinen, einschränken oder aggregieren, sollten Spalten mit echten Datentypen sein. Der Test ist einfach: Wenn Sie feststellen, dass Sie metadata->>'price' in den meisten Abfragen in eine Zahl casten, hätte es eine integer sein sollen.

Ein Mittelweg ist der Hybrid-Ansatz: stabile Felder als Spalten, alles Optionale in einem JSONB attributes Bag. So erhalten Sie Constraints und Indizes dort, wo sie wichtig sind, und Flexibilität dort, wo sie nicht benötigt werden.

Extensions, die die Möglichkeiten erweitern

CREATE EXTENSION installiert ein gebündeltes Modul in eine Datenbank. Einige davon sollte man namentlich kennen:

  • pg_stat_statements — zeichnet normalisierten Query-Text inklusive Timing und I/O auf; das Erste, was man bei Performance-Untersuchungen aktivieren sollte.
  • PostGIS — geografische Typen und Funktionen; der Grund, warum viele Teams Postgres für Standortdaten wählen.
  • pgvector — Vektor-Spalten und Approximate-Nearest-Neighbour-Indizes für Embeddings und semantische Suche.
  • pgcrypto — kryptografische Funktionen wie gen_random_uuid() in älteren Versionen.
  • pg_trgm — Trigramm-Indizes für schnelles LIKE und Fuzzy Matching.

Aktivieren Sie nur das, was Sie tatsächlich nutzen, und bedenken Sie, dass Managed Provider oft eine Einstellung oder eine Support-Anfrage erfordern, bevor eine Extension installiert werden kann.

Rollen und Berechtigungen

Postgres unterscheidet zwischen Rollen (die sich anmelden oder Objekte besitzen können) und Privilegien (was eine Rolle tun darf). Gewähren Sie nur die minimal notwendigen Berechtigungen (Least Privilege).

CREATE ROLE app_user LOGIN PASSWORD 'secret';

GRANT CONNECT ON DATABASE shop TO app_user;
GRANT USAGE ON SCHEMA public TO app_user;
GRANT SELECT, INSERT, UPDATE, DELETE
  ON ALL TABLES IN SCHEMA public TO app_user;

ALTER DEFAULT PRIVILEGES IN SCHEMA public
  GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO app_user;

Die ALTER DEFAULT PRIVILEGES-Zeile ist wichtig, da sie für später erstellte Tabellen gilt und nicht nur für bereits bestehende. Vermeiden Sie es, die Anwendung als Datenbankbesitzer oder Superuser zu verbinden; eine kompromittierte App sollte nicht in der Lage sein, DROP TABLE. Verwenden Sie für Migrationen eine separate Rolle mit DDL-Rechten.

Backups und Recovery

Es gibt zwei Arten von Backups. Logische Backups nutzen pg_dump, um ein portables Skript oder ein Archiv einer einzelnen Datenbank zu erstellen, und pg_dumpall, um Rollen und globale Einstellungen zu erfassen. Sie sind einfach und versionsübergreifend kompatibel, aber bei großen Datenmengen langsamer in der Wiederherstellung.

pg_dump --format=custom --file=shop.dump shop
pg_restore --dbname=shop_restore shop.dump

Physische Backups kopieren das Datenverzeichnis und das Write-Ahead Log (WAL). pg_basebackup zusammen mit einem kontinuierlichen WAL-Archiv ermöglicht eine Point-in-Time Recovery, mit der Sie den Zustand zu einem spezifischen Zeitpunkt vor einer fehlerhaften Migration wiederherstellen können. Dies ist der Ansatz, den Managed Provider verwenden.

Egal für welche Methode Sie sich entscheiden, die Regel bleibt dieselbe: Automatisieren Sie den Prozess, speichern Sie die Backups außerhalb des primären Hosts und stellen Sie diese regelmäßig in einer Test-Datenbank wieder her. Ein Backup, das noch nie erfolgreich wiederhergestellt wurde, ist eine Hoffnung, kein Plan.

Volltextsuche ohne zusätzlichen Service

Postgres verfügt über eine integrierte Volltextsuche, die oft ausreicht, um den Betrieb eines separaten Search-Clusters zu vermeiden. Sie funktioniert, indem Text in einen tsvector aus Lexemen und Abfragen in einen tsquery umgewandelt und diese anschließend abgeglichen werden.

ALTER TABLE posts
  ADD COLUMN search tsvector
    GENERATED ALWAYS AS (
      to_tsvector('english', coalesce(title, '') || ' ' || coalesce(body, ''))
    ) STORED;

CREATE INDEX posts_search_idx ON posts USING gin (search);

SELECT id, title
FROM posts
WHERE search @@ plainto_tsquery('english', 'connection pooling')
ORDER BY ts_rank(search, plainto_tsquery('english', 'connection pooling')) DESC
LIMIT 20;

Eine generierte Spalte hält den Vektor automatisch synchron, und der GIN-Index sorgt für einen schnellen @@-Abgleich. plainto_tsquery wandelt Benutzereingaben sicher in eine Abfrage um, während ts_rank die Ergebnisse nach Relevanz sortiert. Sie können ts_headline hinzufügen, um Treffer in den Ergebnissen hervorzuheben.

Greifen Sie nur dann zu einer dedizierten Suchmaschine, wenn Sie Faceting, skalierbare Fuzzy-Toleranz bei Tippfehlern oder eine dokumentenübergreifende Relevanzoptimierung benötigen. Für die meisten Anwendungen eliminiert die integrierte Version eine komplette Fehlerquelle in der Infrastruktur.

Views und materialized views

Eine View ist eine gespeicherte Abfrage, die sich wie eine Tabelle verhält. Sie speichert keine Daten; sie ist eine benannte Methode, um eine gängige Struktur zu kapseln und Berechtigungen konsistent zu halten.

CREATE VIEW paid_orders AS
SELECT id, user_id, total_cents, created_at
FROM orders
WHERE status = 'paid';

SELECT * FROM paid_orders WHERE created_at >= now() - interval '7 days';

Eine materialized view speichert hingegen das Ergebnis. Dadurch werden aufwendige Aggregationen beim Lesen performant, allerdings auf Kosten der Aktualität der Daten (Staleness).

CREATE MATERIALIZED VIEW daily_revenue AS
SELECT date_trunc('day', created_at) AS day,
       sum(total_cents)             AS revenue_cents
FROM orders
WHERE status = 'paid'
GROUP BY 1;

REFRESH MATERIALIZED VIEW CONCURRENTLY daily_revenue;

CONCURRENTLY aktualisiert die Daten, ohne die Leser zu blockieren, erfordert jedoch einen Unique Index auf der View. Planen Sie die Aktualisierungen entsprechend Ihrer Toleranz für veraltete Zahlen und denken Sie daran, dass eine materialized view ein Cache ist: Sie kann jederzeit aus den Basistabellen neu aufgebaut werden.

Schemas sicher ändern

In einer Live-Datenbank belegen DDL-Statements Locks. Das Ziel ist es, lange ACCESS EXCLUSIVE Locks zu vermeiden, die Lese- und Schreibzugriffe blockieren, während eine Tabelle neu geschrieben wird.

ALTER TABLE orders ADD COLUMN channel text;
ALTER TABLE orders ALTER COLUMN channel SET DEFAULT 'web';

CREATE INDEX CONCURRENTLY orders_channel_idx ON orders (channel);

Das Hinzufügen einer nullable Spalte ohne Default-Wert erfolgt in modernem Postgres sofort, und das Festlegen eines Default-Werts ist ein reiner Metadaten-Vorgang. CREATE INDEX CONCURRENTLY erstellt den Index, ohne einen Write-Lock zu halten, kann jedoch nicht innerhalb einer Transaction ausgeführt werden und kann fehlschlagen, wodurch ein ungültiger Index zurückbleibt, der gelöscht und erneut versucht werden muss. Das Hinzufügen eines NOT NULL Constraints zu einer großen Tabelle sollte schrittweise erfolgen: fügen Sie einen CHECK hinzu, der separat validiert wird, und konvertieren Sie diesen anschließend.

Versionieren Sie jede Änderung als Migration, damit jede Umgebung dasselbe Schema in derselben Reihenfolge erreicht. Bearbeiten Sie eine Produktionstabelle niemals manuell; Sie werden vergessen, was Sie getan haben.

Bulk-Loading mit COPY

Das Einfügen von Zeilen in einzelnen Statements ist der langsamste Weg, um Daten zu laden. Postgres bietet COPY an, wodurch Daten in einem einzigen Befehl gestreamt werden und was um ein Vielfaches schneller sein kann als eine Schleife aus INSERT-Statements.

COPY orders (user_id, status, total_cents, created_at)
FROM '/data/orders.csv'
WITH (FORMAT csv, HEADER true);

COPY (SELECT id, email FROM users WHERE created_at > now() - interval '30 days')
TO '/tmp/recent-users.csv'
WITH (FORMAT csv, HEADER true);

COPY wird auf dem Server ausgeführt, daher muss die Datei für den Datenbankprozess lesbar sein; verwenden Sie stattdessen \copy in psql, um die Daten vom Client aus zu lesen. Für Anwendungscode streamt die copy-API des Treibers Zeilen, ohne eine Datei zwischenzuspeichern. Kapseln Sie einen Ladevorgang in eine Transaktion ein, wenn dieser als Ganzes („all-or-nothing“) erfolgen muss. Bei sehr großen Datenmengen sollten Sie Indizes danach löschen oder neu aufbauen, da die Pflege von Indizes während eines Bulk-Inserts den Hauptkostenfaktor darstellt.

Batch-Inserts sind ein einfacher Mittelweg, wenn COPY unpraktikabel ist:

INSERT INTO orders (user_id, status, total_cents)
VALUES (1, 'paid', 1999),
       (2, 'paid', 4599),
       (3, 'pending', 999);

Best Practices

  • Wähle text, bigint, numeric, timestamptz und jsonb bewusst aus, anstatt standardmäßig auf varchar und Floats zurückzugreifen.
  • Deklariere NOT NULL, UNIQUE, CHECK und Foreign Keys in derselben Migration, in der die Tabelle erstellt wird.
  • Entwirf Indexe basierend auf realen Queries und verifiziere diese mit EXPLAIN ANALYZE.
  • Halte Transaktionen kurz und wähle das niedrigste Isolationslevel, das noch korrekt funktioniert.
  • Setze einen Pooler wie PgBouncer vor die Datenbank und nutze zusätzlich Pooling in der Anwendung.
  • Verwende JSONB für tatsächlich variable Daten und nicht als Abkürzung, um das Schema-Design zu umgehen.
  • Gewähre Anwendungsrollen nur die minimal notwendigen Berechtigungen (Least Privilege) und behalte DDL in einer Migrations-Rolle.
  • Aktiviere pg_stat_statements, überwache den Autovacuum-Prozess und stelle Backups regelmäßig testweise wieder her.
  • Versioniere jede Schema-Änderung als Migration, damit die Umgebungen synchron bleiben.

Häufige Fehler

  • Geldbeträge in double precision speichern und durch Rundungsfehler Cent-Beträge verlieren.
  • timestamp anstelle von timestamptz verwenden und versehentlich lokale Zeiten speichern.
  • WHERE bei einem UPDATE oder DELETE vergessen und dadurch jede einzelne Zeile aktualisieren.
  • Für jede Spalte einen eigenen Index erstellen, anstatt zusammengesetzte Indizes (Composite Indexes) zu nutzen, die auf die Abfrage zugeschnitten sind.
  • Pro Request eine neue Verbindung öffnen und so max_connections erschöpfen.
  • Eine Transaktion über einen HTTP-Aufruf oder eine Benutzerinteraktion hinweg offen halten.
  • NULL als gleichbedeutend mit NULL behandeln; Vergleiche erfordern IS NULL und IS NOT NULL.
  • Davon ausgehen, dass ein Index verwendet wird, ohne den Query-Plan zu prüfen.
  • Anwendungsabfragen als Datenbankbesitzer oder Superuser ausführen.

Wie geht es weiter?

Postgres belohnt Tiefe, und der nächste logische Schritt ist die fließende Beherrschung seiner Sprache: lies den SQL-Guide, um Joins, CTEs und Window Functions zu vertiefen. Wenn du Alternativen abwägst, behandelt MySQL die andere dominierende Open-Source-relationale Datenbank, MongoDB erklärt das Dokumentenmodell und Redis zeigt die Caching-Schicht, die normalerweise vor einem relationalen Store sitzt.

In der Praxis

Schema, Query, Plan, Transaktion

Die vier Dinge, die du am häufigsten tust: eine Tabelle definieren, sie joinen, sie indexieren und sie atomar ändern.

schema.sql
CREATE TABLE users (
  id         bigserial PRIMARY KEY,
  email      text NOT NULL UNIQUE,
  created_at timestamptz NOT NULL DEFAULT now()
);

CREATE TABLE orders (
  id          bigserial PRIMARY KEY,
  user_id     bigint NOT NULL REFERENCES users (id) ON DELETE CASCADE,
  status      text NOT NULL DEFAULT 'pending'
                CHECK (status IN ('pending', 'paid', 'shipped', 'cancelled')),
  total_cents integer NOT NULL CHECK (total_cents >= 0),
  created_at  timestamptz NOT NULL DEFAULT now(),
  metadata    jsonb NOT NULL DEFAULT '{}'::jsonb
);

Modellierung semi-strukturierter Daten

JSONB ist exzellent für wirklich optionale Attribute, aber Felder, nach denen gefiltert oder die eingeschränkt werden, gehören in Spalten.

Bevorzugt
CREATE TABLE products (
  id         bigserial PRIMARY KEY,
  name       text NOT NULL,
  price_cents integer NOT NULL,
  attributes jsonb NOT NULL DEFAULT '{}'::jsonb
);

CREATE INDEX products_attributes_idx
  ON products USING gin (attributes);
Vermeiden
CREATE TABLE products (
  id   bigserial PRIMARY KEY,
  data jsonb NOT NULL
);

-- Every query now casts text and the database
-- cannot enforce that price is a number.
SELECT (data->>'price')::int FROM products;

Indexierung für die ausgeführte Query

Ein Index sollte zum WHERE und ORDER BY einer realen Query passen. Jede Spalte zu indexieren verlangsamt Schreibvorgänge und bringt keinen Nutzen.

Bevorzugt
CREATE INDEX orders_user_created_idx
  ON orders (user_id, created_at DESC);
Vermeiden
CREATE INDEX ON orders (id);
CREATE INDEX ON orders (user_id);
CREATE INDEX ON orders (status);
CREATE INDEX ON orders (created_at);
-- four single-column indexes that a
-- composite index would cover alone

Abwägungen

Sollte Postgres deine Standard-Datenbank sein?

Postgres passt für die überwältigende Mehrheit der Anwendungen. Es lohnt sich jedoch zu wissen, wo es mehr von dir verlangt.

Strengths

  • Korrektheit ohne Babysitting

    Constraints, echte Transaktionen und starke Typen bedeuten, dass die Datenbank ungültige Zustände ablehnt, sodass Anwendungsfehler Daten nicht stillschweigend korrumpieren können.

  • Eine Datenbank für viele Formen

    Relationale Tabellen, JSONB-Dokumente, Volltextsuche und sogar Vector Embeddings koexistieren, was viel operationalen Aufwand reduziert.

  • Eine gesunde, unabhängige Community

    Die Entwicklung ist offen und herstellerneutral, Releases sind vorhersehbar und Managed-Angebote existieren auf jeder großen Cloud.

Trade-offs

  • Verbindungen sind nicht kostenlos

    Jede Verbindung ist ein Backend-Prozess. Ohne Pooler können einige hundert Anwendungsclients den Speicher erschöpfen und max_connections erreichen.

  • Vacuum ist eine echte Aufgabe

    MVCC hinterlässt tote Tuples. Autovacuum bewältigt dies meist, aber schwere Update-Workloads benötigen Monitoring und gelegentliches Tuning.

  • Tuning belohnt Erfahrung

    Die Defaults sind vernünftig, doch work_mem, shared_buffers und Planner-Einstellungen sind unter Last entscheidend und brauchen Zeit, um sie wirklich zu beherrschen.

Häufig gestellte Fragen

Häufig gestellte Fragen

Keep learning

Related topics from the roadmap.

$ Lernen Sie jetzt

Bereit, PostgreSQL zu lernen?

Unser interaktives Tutorial führt Sie Schritt für Schritt durch PostgreSQL — mit Quizzen und echtem Code, den Sie im Browser ausführen können.