Ein Schema in PostgreSQL ist ein Namespace innerhalb einer Datenbank. Es gruppiert Tabellen, Views, Sequenzen, Funktionen und Typen und sorgt dafür, dass es zwei users-Tabellen geben darf, solange sie in verschiedenen Schemas liegen. Wer eine Query schreibt, ohne das Schema explizit zu nennen, vertraut auf den search_path — und genau hier passieren die meisten Überraschungen.

Schema = Namespace

Eine Datenbank besteht aus mehreren Schemas. Jeder Schema-Eintrag ist ein eigener Container für Objekte:

SQL Schema anlegen und nutzen
CREATE SCHEMA app;
CREATE TABLE app.orders (id serial primary key, total numeric);
CREATE TABLE app.customers (id serial primary key, email text);

Ein Postgres-Cluster bringt von Haus aus mehrere Schemas mit:

SchemaZweck
publicDefault-Schema für eigene Objekte
pg_catalogSystem-Tabellen (pg_class, pg_attribute, …)
information_schemaSQL-Standard-Sichten auf den Katalog
pg_toastInterne Speicherung großer Werte (TOAST)
pg_temp_*Pro-Session temporäre Schemas

pg_catalog ist immer verfügbar — auch ohne USAGE-Privileg.

search_path — wer wird zuerst gefunden?

Wenn du SELECT * FROM orders; schreibst, ohne ein Schema zu nennen, sucht Postgres die Tabelle im search_path ab — der Reihe nach.

SQL search_path anzeigen und setzen
SHOW search_path;
--  search_path
-- ---------------
--  "$user", public

SET search_path TO app, public;

"$user" ist ein Sonderwert: Postgres ersetzt ihn zur Laufzeit durch den Namen der aktuellen Rolle. Wenn ein User alice heißt und ein Schema alice existiert, wird das durchsucht. Existiert es nicht, wird der Eintrag still übersprungen.

Mit SET search_path TO app, public; legst du fest: erst in app suchen, dann in public.

Schreibvorgänge (CREATE TABLE foo … ohne Schema-Präfix) landen immer im ersten existierenden Schema des search_path — also bei obigem Setting in app.

search_path persistent setzen

SET gilt nur für die aktuelle Session. Für persistenten Effekt:

SQL Pro Rolle
ALTER ROLE app_user SET search_path = app, public;
SQL Pro Datenbank
ALTER DATABASE myapp SET search_path = app, public;

Beide Varianten greifen ab der nächsten Session. Wer sich gerade verbunden hat, hat noch den alten Wert.

Auch in Funktionen kann der search_path lokal gesetzt werden:

SQL
CREATE FUNCTION calculate_tax(amount numeric)
RETURNS numeric
LANGUAGE sql
SET search_path = app, pg_temp
AS $$
    SELECT amount * (SELECT rate FROM tax_rates LIMIT 1);
$$;

Das ist insbesondere bei SECURITY DEFINER-Funktionen Pflicht — sonst öffnet man Lücken (siehe Pitfall unten).

Das public-Schema — Geschichte und PG 15

Bis PostgreSQL 14 hatte das public-Schema sehr permissive Defaults: jede Rolle (PUBLIC) durfte dort CREATE und USAGE. Das war bequem für Einsteiger und gefährlich für alle anderen, die mit mehreren Usern arbeiteten.

Seit PostgreSQL 15 ist public deutlich restriktiver:

Privileg auf publicBis PG 14Ab PG 15
USAGEfür PUBLIC (jeder)für PUBLIC (jeder) — unverändert
CREATEfür PUBLIC (jeder)nur für Owner (postgres bzw. DB-Owner)

Praktische Folge nach Major-Upgrade: Anwendungen, die ihre Tabellen im public-Schema anlegen wollen, brauchen entweder ein eigenes Schema (sauberer) oder explizites GRANT CREATE ON SCHEMA public TO app_user;.

Schema-basierte Mandantentrennung

Eine elegante Methode für Multi-Tenancy: ein Schema pro Mandant, alle Tabellen identisch strukturiert.

SQL Pro Tenant ein Schema
CREATE SCHEMA tenant_acme;
CREATE SCHEMA tenant_globex;

-- Identisches Schema-Layout in beiden:
CREATE TABLE tenant_acme.orders   (id serial primary key, total numeric);
CREATE TABLE tenant_globex.orders (id serial primary key, total numeric);

Pro Login-Rolle ein search_path:

SQL
CREATE ROLE acme_user LOGIN PASSWORD 'changeme';
ALTER ROLE acme_user SET search_path = tenant_acme;

acme_user sieht nur Daten aus tenant_acme — dieselbe Anwendung, dieselben Queries, andere Daten.

Vorteile:

  • starke Isolation auf DB-Ebene (kein versehentlicher Cross-Tenant-Zugriff durch fehlerhafte WHERE-Klausel)
  • pro-Tenant-Backup einfacher (pg_dump --schema=tenant_acme)
  • pro-Tenant-Migration möglich

Nachteile:

  • Schema-Updates müssen für jeden Tenant durchgeführt werden (Migrations-Tooling muss das können)
  • Anzahl Tenants begrenzt (tausende Schemas können den Katalog aufblähen)
  • Cross-Tenant-Reporting wird umständlich

In der Praxis: gut für mittlere Tenant-Zahlen (10–500), nicht für SaaS mit zehntausenden Kunden — dort ist Row-Level Security mit Tenant-ID-Spalte oft besser.

Häufige Stolperfallen

USAGE auf das Schema fehlt — die Klassikerfalle.

GRANT SELECT ON ALL TABLES IN SCHEMA app TO reader; reicht nicht. Ohne GRANT USAGE ON SCHEMA app TO reader; bekommt der Reader weiterhin „permission denied for schema app". Das passiert besonders gerne, wenn man Predefined Roles wie pg_read_all_data umgehen will und manuell granted.

search_path und SECURITY DEFINER — das Sicherheitsleck.

Eine SECURITY DEFINER-Funktion läuft mit den Rechten des Owners. Wenn der search_path zur Aufrufzeit eine schadhafte Schema-Reihenfolge enthält, kann ein Angreifer eigene Funktionen vorne schmuggeln und die Owner-Rechte kapern. Lösung: in jeder SECURITY DEFINER-Funktion SET search_path = pg_catalog, pg_temp (oder explizit nur die Schemas, die du brauchst) setzen.

Schema-Renaming bricht Anwendungs-Code.

ALTER SCHEMA tenant_acme RENAME TO tenant_acme_corp; ist syntaktisch trivial — aber jede Connection-Konfiguration und jeder hardcodierte Schema-Name (z. B. in Migrationsskripten oder Views) bricht. Vor dem Rename die Codebase grep'pen.

DROP SCHEMA … CASCADE entfernt ALLES darin.

Inklusive Tabellen mit Daten, Sequenzen, Funktionen, Trigger. Das ist gewollt, aber gerne unterschätzt. Vor CASCADE ein Backup machen oder mit DROP SCHEMA tenant_acme; (ohne CASCADE) prüfen, was sich beschwert — das gibt einen Eindruck vom Umfang.

Default-Privileges sind pro Schema — nicht magisch global.

ALTER DEFAULT PRIVILEGES IN SCHEMA app … greift nur für Tabellen, die in app entstehen. Wer in tenant_acme neue Tabellen erstellt, braucht eine separate Default-Privilege-Definition. Bei Multi-Tenant-Setups ist das ein Bootstrap-Schritt pro neuem Tenant.

search_path mit nicht-existierenden Schemas wirft keinen Fehler.

SET search_path = analytics, app, public; — wenn analytics nicht existiert, geht das durch ohne Warnung. Erst bei der nächsten Query, die einen Identifier sucht, wird übersprungen. Bei Refactoring von Schemas keine RAISE WARNING zu erwarten — manueller \dn-Check schadet nicht.

Weiterführende Ressourcen

Externe Quellen

/ Weiter

Zurück zu Server-Administration

Zur Übersicht