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