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übervarchar(n), es sei denn, das Limit ist eine echte Business-Regel.integerundbigint— ganze Zahlen. Nutzebigintfü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, nichtrealoderdouble precision.timestamptz— ein in UTC gespeicherter Zeitstempel mit Zeitzonen-Unterstützung. Bevorzuge diesen Typ für Anwendungsdaten immer gegenübertimestamp.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
LIKEund 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,timestamptzundjsonbbewusst aus, anstatt standardmäßig aufvarcharund Floats zurückzugreifen. - Deklariere
NOT NULL,UNIQUE,CHECKund 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 precisionspeichern und durch Rundungsfehler Cent-Beträge verlieren. timestampanstelle vontimestamptzverwenden und versehentlich lokale Zeiten speichern.WHEREbei einemUPDATEoderDELETEvergessen 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_connectionserschöpfen. - Eine Transaktion über einen HTTP-Aufruf oder eine Benutzerinteraktion hinweg offen halten.
NULLals gleichbedeutend mitNULLbehandeln; Vergleiche erfordernIS NULLundIS 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.