22. Mai 2025·5 Min. Lesezeit

PostgreSQL-generierte Spalten für schnellere Admin-Filter

Erfahre, wie PostgreSQL-generierte Spalten Filter und Sortierung in Admin-Oberflächen beschleunigen können und zugleich das SQL lesbar bleiben lässt – mit praktischen Beispielen und Schnellchecks.

PostgreSQL-generierte Spalten für schnellere Admin-Filter

Warum Admin-Oberflächen schnell chaotisch und langsam werden

Admin-Bildschirme beginnen meist einfach: eine Tabelle, ein paar Filter, vielleicht eine Sortierung nach "neueste zuerst". Dann fängt die eigentliche Arbeit an. Support will eine Suche, die Kunden nach Name, E-Mail und Telefon findet. Sales möchte nach „letzter Aktivität“ sortieren. Finance will „überfälliges Guthaben“. Jede Anforderung fügt Bedingungen, Joins und zusätzliche Berechnungen hinzu.

Die meisten Admin-Listen werden aus einem Grund langsam: jeder Klick verändert die Abfrage. Filtern und Sortieren kann die Datenbank dazu bringen, viele Zeilen zu scannen — besonders wenn die Abfrage für jede Zeile einen Wert berechnen muss, bevor sie entscheidet, ob die Zeile passt.

Ein häufiger Wendepunkt ist, wenn WHERE und ORDER BY sich mit Ausdrücken füllen. Statt auf eine einfache Spalte zu filtern, filtert man auf lower(email), date_trunc('day', last_seen_at) oder eine CASE-Anweisung, die mehrere Status zu einem „Bucket“ zusammenfasst. Diese Ausdrücke sind nicht nur langsamer. Sie machen das SQL schwerer lesbar, schwerer zu indexieren und leichter fehleranfällig.

Unordentliches Admin-SQL entsteht meist aus einigen wiederkehrenden Mustern:

  • Ein einzelnes „Search“-Eingabefeld, das mehrere Felder mit unterschiedlichen Regeln prüft
  • Sortierung nach einem abgeleiteten Wert (voller Name, Prioritäts-Score, „letztes relevantes Ereignis“)
  • Geschäftsregeln, die über Bildschirme hinweg kopiert werden (aktiv vs. inaktiv, bezahlt vs. überfällig)
  • Kleine "Hilfs"-Anpassungen (trim, lower, coalesce) überall verstreut
  • Derselbe berechnete Wert, der in Liste, Filtern und Sortierung verwendet wird

Teams versuchen oft, das in der App-Schicht zu verbergen: dynamische Query-Builder, bedingte Joins oder Vorberechnungen im Code. Das kann funktionieren, aber es verteilt die Logik zwischen UI und Datenbank, was das Debuggen langsamer Abfragen erschwert.

Das Ziel ist einfach: schnelle Abfragen, die lesbar bleiben. Wenn ein berechneter Wert immer wieder auf Admin-Bildschirmen auftaucht, können PostgreSQL-generierte Spalten die Regel an einem Ort halten und gleichzeitig der Datenbank erlauben, zu optimieren.

Generierte Spalten einfach erklärt

Eine generierte Spalte ist eine normale Tabellenspalte, deren Wert aus anderen Spalten berechnet wird. Du legst den Wert nicht selbst fest. PostgreSQL füllt ihn anhand eines von dir definierten Ausdrucks.

In PostgreSQL sind generierte Spalten gespeichert. PostgreSQL berechnet den Wert beim Einfügen oder Aktualisieren einer Zeile und speichert ihn wie jede andere Spalte auf der Festplatte. Das ist in der Regel das, was du für Admin-Bildschirme willst: schnelle Lesezugriffe und die Möglichkeit, den berechneten Wert zu indexieren.

Das unterscheidet sich davon, die gleiche Berechnung in jeder Abfrage vorzunehmen. Wenn du immer wieder WHERE lower(email) = lower($1) schreibst oder nach last_name || ', ' || first_name sortierst, bezahlst du die Kosten mehrfach und dein SQL wird unübersichtlich. Eine generierte Spalte verschiebt diese wiederholte Rechnung in die Tabellen-Definition. Deine Abfragen werden einfacher und das Ergebnis ist überall konsistent.

Wenn sich Quelldaten ändern, aktualisiert PostgreSQL den generierten Wert automatisch für diese Zeile. Deine App muss sich nicht darum kümmern, ihn synchron zu halten.

Ein nützliches mentales Modell:

  • Definiere die Formel einmal.
  • PostgreSQL berechnet sie beim Schreiben.
  • Abfragen lesen sie wie eine normale Spalte.
  • Da sie gespeichert ist, kannst du sie indexieren.

Wenn du später die Formel änderst, brauchst du eine Schemaänderung. Plane das wie jede Migration, denn bestehende Zeilen werden aktualisiert, damit sie dem neuen Ausdruck entsprechen.

Gute Einsatzfälle für berechnete Felder in Filtern und Sortierung

Generierte Spalten glänzen, wenn der Wert immer aus anderen Spalten abgeleitet ist und du oft danach filterst oder sortierst. Für einmalige Berichte sind sie weniger hilfreich.

Suchfreundliche Felder, die Menschen auch wirklich nutzen

Admin-Suche ist selten „reine“ Suche. Leute erwarten, dass das Feld mit unordentlichem Text, unterschiedlicher Groß-/Kleinschreibung und zusätzlichen Leerzeichen klarkommt. Wenn du einen generierten "search key" speicherst, der bereits normalisiert ist, bleibt dein WHERE lesbar und verhält sich überall gleich.

Gute Kandidaten sind ein kombinierter voller Name, kleingeschriebener und getrimmter Text für case-insensitive Suche, eine bereinigte Version, die Mehrfachleerzeichen zusammenfasst, oder ein Statuslabel, das aus mehreren Feldern abgeleitet ist.

Beispiel: statt lower(trim(first_name || ' ' || last_name)) in jeder Abfrage zu wiederholen, erzeuge einmal full_name_key und filtere danach.

Sortierschlüssel, wie Menschen sortieren

Sortierung ist oft der Bereich, in dem sich berechnete Felder am schnellsten auszahlen, weil Sortieren PostgreSQL dazu bringen kann, Ausdrücke für viele Zeilen auszuwerten.

Gängige Sortierschlüssel sind eine numerische Rangfolge (Plan-Tier auf 1, 2, 3 abgebildet), ein einzelner „letzter Aktivitäts“-Zeitstempel (z. B. das Maximum aus zwei Zeitstempeln) oder ein gepaddeter Code, der als Text korrekt sortiert.

Wenn der Sortierschlüssel eine einfache indexierte Spalte ist, wird ORDER BY deutlich günstiger.

Abgeleitete Flags für schnelle Filter

Admin-Benutzer lieben Checkboxen wie „Überfällig“ oder „Hoher Wert“. Diese eignen sich gut als generierte Spalten, wenn die Logik stabil ist und nur auf Zeilendaten basiert.

Beispielsweise, wenn eine Kundenliste „Hat ungelesene Nachrichten“ und „Ist überfällig“ braucht, macht eine generierte has_unread-Boolean (aus unread_count > 0) und is_overdue (aus due_date < now() und paid_at is null) UI-Filter zu einfachen Bedingungen.

Auswahl: generierte Spalten, Indizes und andere Optionen

Admin-Bildschirme brauchen drei Dinge: schnelles Filtern, schnelles Sortieren und SQL, das man auch Monate später noch lesen kann. Die echte Entscheidung ist, wo die Berechnung liegen soll: in der Tabelle, im Index, in einer View oder im App-Code.

Generierte Spalten passen gut, wenn du willst, dass der Wert sich wie eine echte Spalte verhält: leicht referenzierbar, sichtbar in SELECTs und nicht zu übersehen, wenn neue Filter hinzukommen. Sie lassen sich gut mit normalen Indizes kombinieren.

Expression-Indexes sind oft schneller zu ergänzen, weil du die Tabellenstruktur nicht änderst. Wenn dir vor allem die Geschwindigkeit wichtig ist und dir unordentliches SQL nichts ausmacht, reicht oft ein Ausdrucksindex. Nachteilig ist die Lesbarkeit und die Abhängigkeit davon, dass der Planner genau deinen Ausdruck wiedererkennt.

Views sind sinnvoll, wenn du eine gemeinsame Datenform haben willst, besonders wenn deine Admin-Liste viele Joins enthält. Komplexe Views können jedoch teure Arbeit verbergen und eine zweite Fehlerquelle beim Debuggen sein.

Trigger können eine normale Spalte synchron halten, aber sie sind zusätzliche Teile, die man verwalten muss. Sie können Bulk-Updates verlangsamen und beim Troubleshooting übersehen werden.

Manchmal ist die beste Option eine normale Spalte, die die App füllt. Wenn Benutzer sie bearbeiten können oder sich die Formel oft ändert basierend auf Geschäftsentscheidungen (nicht nur Zeilendaten), ist explizites Speichern klarer.

Eine schnelle Entscheidungsregel:

  • Willst du lesbare Abfragen und eine stabile Formel, die nur auf Zeilendaten basiert? Verwende eine generierte Spalte.
  • Willst du Geschwindigkeit für einen spezifischen Filter und stört dich unordentliches SQL nicht? Verwende einen Ausdrucksindex.
  • Brauchst du eine verbundene, reportartige Form, die an vielen Stellen wiederverwendet wird? Ziehe eine View in Betracht.
  • Brauchst du Logik über mehrere Tabellen oder Nebeneffekte? Bevorzuge App-Logik zuerst, Trigger zuletzt.

Schritt für Schritt: eine generierte Spalte hinzufügen und in einer Abfrage nutzen

Business-Regeln wiederverwenden
Definiere Status-Buckets und Flags einmal, damit jede Admin-Ansicht übereinstimmt.
Projekt starten

Beginne mit einer langsamen Admin-Liste, die du im UI spürst. Schreib die Filter und Sortierungen auf, die der Screen am meisten nutzt. Verbessere zuerst diese einzelne Abfrage.

Wähle ein berechnetes Feld, das wiederholte Arbeit entfernt, und gib ihm einen klaren snake_case-Namen, damit andere ohne erneutes Lesen des Ausdrucks verstehen, was es enthält.

1) Die generierte Spalte (STORED) hinzufügen

ALTER TABLE customers
ADD COLUMN full_name_key text
GENERATED ALWAYS AS (
  lower(concat_ws(' ', last_name, first_name))
) STORED;

Validiere an echten Zeilen, bevor du Indizes hinzufügst:

SELECT id, first_name, last_name, full_name_key
FROM customers
ORDER BY id DESC
LIMIT 5;

Wenn die Ausgabe falsch ist, behebe den Ausdruck jetzt. STORED bedeutet, PostgreSQL hält den Wert bei jedem Insert und Update aktuell.

2) Den Index hinzufügen, der zu deinem Admin-Screen passt

Wenn dein Admin-Screen nach status filtert und nach Name sortiert, indexiere dieses Muster:

CREATE INDEX customers_status_full_name_key_idx
ON customers (status, full_name_key);

3) Die Admin-Abfrage aktualisieren

Früher hattest du vielleicht ein unübersichtliches ORDER BY. Danach ist es klarer:

SELECT id, status, first_name, last_name
FROM customers
WHERE status = 'active'
ORDER BY full_name_key ASC
LIMIT 50 OFFSET 0;

Verwende generierte Spalten für die Teile, nach denen Menschen täglich filtern und sortieren, nicht für selten genutzte Screens.

Indexierungsmuster, die echten Admin-Screens entsprechen

Vom Modell zum Code
Vom Modell zum Code: no-code bauen und trotzdem echten Go-, Vue3- und nativen Mobile-Quellcode erhalten.
Code generieren

Admin-Screens wiederholen ein paar Verhaltensweisen: Filtern nach einer Handvoll Felder, Sortieren nach einer Spalte und Paginieren. Die beste Lösung ist selten "indexiere alles". Es ist eher: "indexiere die exakte Form der häufigsten Abfragen".

Eine praktische Regel: Setze die häufigsten Filterspalten zuerst und die häufigste Sortierspalte zuletzt. Bei Multi-Tenant-Systemen kommt oft workspace_id (oder ähnlich) zuerst: (workspace_id, status, created_at).

Textsuche ist ein eigenes Thema. Viele Suchfelder enden bei ILIKE '%term%', was sich mit einfachen btree-Indizes schwer beschleunigen lässt. Ein hilfreiches Muster ist, auf einer normalisierten Hilfsspalte statt auf dem Rohtext zu suchen (kleingeschrieben, getrimmt, eventuell verkettet). Wenn dein UI Präfixsuche (term%) unterstützen kann, hilft ein btree-Index auf dieser normalisierten Spalte. Muss es eine Contains-Suche (%term%) sein, erwäge, das UI-Verhalten bei großen Tabellen einzuschränken (z. B. „E-Mail beginnt mit“), oder beschränke die Suche auf eine kleinere Teilmenge.

Prüfe auch die Selektivität, bevor du Indizes hinzufügst. Wenn 95% der Zeilen denselben Wert teilen (z. B. status = 'active'), hilft ein einzelner Index auf dieser Spalte kaum. Kombiniere ihn mit einer selektiveren Spalte oder verwende einen partiellen Index für den Minderheitsfall.

Realistisches Beispiel: eine schnelle Kunden-Admin-Liste

Stell dir eine typische Kunden-Admin-Seite vor: ein Suchfeld, ein paar Filter (inaktiv, Kontostand-Bereiche) und eine sortierbare Spalte „Last seen“. Im Laufe der Zeit wird das SQL unleserlich: LOWER(), TRIM(), COALESCE(), Datums-Rechnungen und CASE-Blöcke, die überall wiederholt werden.

Eine Möglichkeit, das schnell und lesbar zu halten, ist, diese wiederholten Ausdrücke in generierte Spalten zu schieben.

Tabelle und generierte Spalten

Angenommen, eine customers-Tabelle hat name, email, last_seen und balance. Füge drei berechnete Felder hinzu:

  • search_key: ein normalisierter Text-Blob für einfache Suchen
  • is_inactive: ein Boolean, auf den man filtern kann, ohne Datumslogik zu wiederholen
  • balance_bucket: ein Label für schnelle Segmentierung
ALTER TABLE customers
  ADD COLUMN search_key text
    GENERATED ALWAYS AS (
      lower(trim(coalesce(name, ''))) || ' ' || lower(trim(coalesce(email, '')))
    ) STORED,
  ADD COLUMN is_inactive boolean
    GENERATED ALWAYS AS (
      last_seen IS NULL OR last_seen < (now() - interval '90 days')
    ) STORED,
  ADD COLUMN balance_bucket text
    GENERATED ALWAYS AS (
      CASE
        WHEN balance < 0 THEN 'negative'
        WHEN balance < 100 THEN '0-99'
        WHEN balance < 500 THEN '100-499'
        ELSE '500+'
      END
    ) STORED;

Jetzt liest sich die Admin-Abfrage wie die UI.

Lesbares Filtern + Sortieren

„Inaktive Kunden, neueste Aktivität zuerst“ wird:

SELECT id, name, email, last_seen, balance
FROM customers
WHERE is_inactive = true
ORDER BY last_seen DESC NULLS LAST
LIMIT 50;

Und eine grundlegende Suche wird:

SELECT id, name, email, last_seen, balance
FROM customers
WHERE search_key LIKE '%' || lower(trim($1)) || '%'
ORDER BY last_seen DESC NULLS LAST
LIMIT 50;

Der eigentliche Gewinn ist Konsistenz. Dieselben Felder treiben mehrere Bildschirme an, ohne Logik zu wiederholen:

  • Das Suchfeld in der Kundenliste verwendet search_key
  • Der Tab „Inaktive Kunden“ verwendet is_inactive
  • Balance-Filter-Chips verwenden balance_bucket

Häufige Fehler und Fallen

Gängige Module schnell hinzufügen
Füge Authentifizierung und Stripe-Zahlungsmodule hinzu, wenn dein Admin-Tool sie braucht.
Module hinzufügen

Generierte Spalten können wie ein einfacher Gewinn aussehen: pack die Berechnung in die Tabelle und halte die Abfragen sauber. Sie helfen jedoch nur, wenn sie zur Art passen, wie der Screen filtert und sortiert, und wenn du den richtigen Index hinzufügst.

Die häufigsten Fehler:

  • Zu erwarten, dass es ohne Index schneller wird. Ein berechneter Wert braucht bei großem Umfang trotzdem einen Index für schnelles Filtern oder Sortieren.
  • Zu viel Logik in ein Feld packen. Wenn eine generierte Spalte zum Mini-Programm wird, verlieren Leute das Vertrauen. Halte sie kurz und benenne sie eindeutig.
  • Nicht-immutable-Funktionen verwenden. PostgreSQL verlangt, dass der Ausdruck für eine gespeicherte generierte Spalte immutable ist. Dinge wie now() und random() sind problematisch und oft nicht erlaubt.
  • Schreiblatenz ignorieren. Inserts und Updates müssen den berechneten Wert pflegen. Schnellere Lesefälle sind nichts wert, wenn Importe und Integrationen zu sehr verlangsamt werden.
  • Nahezu-Duplikate erstellen. Standardisiere ein oder zwei Muster (z. B. einen normalisierten Key) statt fünf ähnliche Spalten anzuhäufen.

Wenn deine Admin-Liste Contains-Suchen (ILIKE '%ann%') verwendet, reicht eine generierte Spalte allein meist nicht. Du brauchst eventuell einen anderen Suchansatz. Für die täglichen "filter & sort"-Abfragen sind generierte Spalten plus der richtige Index jedoch häufig eine verlässlichere Performance-Grundlage.

Schnelle Checkliste vor dem Rollout

Schnellere Admin-Tabellen bauen
Erstelle Admin-Listen in AppMaster, die bei wachsender Anzahl an Filtern und Sortierungen schnell bleiben.
Starten

Bevor du Änderungen auf einen Admin-Bildschirm ausrollst, prüfe, ob der berechnete Wert, die Abfrage und der Index zusammenpassen:

  • Die Formel ist stabil und in einem Satz erklärbar.
  • Deine Abfrage nutzt die generierte Spalte in WHERE und/oder ORDER BY.
  • Der Index passt zur echten Nutzung, nicht nur zu einem einmaligen Test.
  • Du hast die Ergebnisse mit der alten Logik bei Randfällen verglichen (NULLs, leere Strings, ungewöhnliche Leerzeichen, gemischte Groß-/Kleinschreibung).
  • Du hast die Schreibleistung getestet, wenn die Tabelle stark genutzt wird (Imports, Hintergrund-Updates, Integrationen).

Nächste Schritte: auf deine Admin-Screens anwenden

Wähle einen kleinen, wirkungsvollen Startpunkt: die 2–3 Admin-Bildschirme, die Leute den ganzen Tag öffnen (Orders, Customers, Tickets). Notiere, was langsam wirkt (Datumsbereichsfilter, Sortierung nach „letzter Aktivität“, Suche nach kombiniertem Namen, Filtern nach einem Statuslabel). Standardisiere dann eine kurze Menge an berechneten Feldern, die du über Bildschirme wiederverwenden kannst.

Ein einfacher, messbarer und rückgängig zu machender Rollout-Plan:

  • Füge die generierte(n) Spalte(n) mit klaren Namen hinzu.
  • Führe alte und neue Logik kurz parallel aus, wenn du vorhandene Logik ersetzt.
  • Erstelle den Index, der den Hauptfilter oder die Hauptsortierung abdeckt.
  • Schalte die Bildschirm-Abfrage auf die neue Spalte um.
  • Messe vorher/nachher (Abfragezeit und gescannte Zeilen) und entferne dann das alte Workaround.

Wenn du interne Admin-Tools in AppMaster (appmaster.io) baust, passen diese berechneten Felder gut in ein gemeinsames Datenmodell: die Datenbank trägt die Regel und deine UI-Filter können auf einen eindeutigen Feldnamen zeigen, statt Ausdrücke über Bildschirme zu verteilen.

FAQ

Wann sollte ich eine PostgreSQL generated column für einen Admin-Bildschirm verwenden?

Generierte Spalten helfen, wenn du denselben Ausdruck immer wieder in WHERE oder ORDER BY verwendest — zum Beispiel Normalisierung von Namen, Mapping von Status oder Bau eines Sortierschlüssels. Sie sind besonders nützlich für Admin-Listen, die den ganzen Tag geöffnet sind und vorhersehbare Filter- und Sortierverhalten brauchen.

Was ist der Unterschied zwischen einer gespeicherten generated column und einem expression index?

Eine gespeicherte (stored) generierte Spalte wird bei Insert oder Update berechnet und wie eine normale Spalte gespeichert, sodass Lesezugriffe schnell sind und indiziert werden können. Ein Ausdrucks-Index speichert das Ergebnis im Index, ohne die Tabellenstruktur zu ändern, aber die Abfrage muss den exakten Ausdruck verwenden, damit der Planner ihn nutzt.

Macht eine generated column meine Abfrage automatisch schneller?

Nein, nicht automatisch. Eine generierte Spalte macht die Abfrage vor allem übersichtlicher und erleichtert das Indexieren eines berechneten Werts. Für echte Performance bei großen Datenmengen brauchst du zusätzlich einen passenden Index auf diesem Feld.

Welche generated columns sind am besten für Suche und Sortierung im Admin geeignet?

Üblicherweise sind das Felder, die du ständig filterst oder sortierst: ein normalisierter Suchschlüssel, ein Sortier-Schlüssel für den vollständigen Namen, ein abgeleitetes Boolean wie is_overdue oder eine Ranking-Zahl, die dem erwarteten Sortierverhalten entspricht. Wähle einen Wert, der wiederholte Arbeit in vielen Abfragen entfernt, nicht eine einmalige Berechnung.

Wie wähle ich den richtigen Index für eine Admin-Liste mit Filter und Sortierung?

Beginne mit den häufigsten Filterspalten und setze den Haupt-Sortierschlüssel ans Ende, z. B. (workspace_id, status, full_name_key), wenn das zum Bildschirm passt. So kann PostgreSQL zuerst filtern und dann die Zeilen bereits in Ordnung zurückgeben, ohne viel Extraarbeit.

Können generated columns langsame Contains-Suchen wie ILIKE '%term%' beheben?

Nicht gut. Eine generierte Spalte kann Text vereinheitlichen, aber ILIKE '%term%' bleibt auf großen Tabellen langsam für einfache btree-Indizes. Wenn Performance kritisch ist, bevorzuge Präfix-Suche (term%), schränke die zu durchsuchende Menge mit weiteren Filtern ein oder passe das UI für große Tabellen an.

Kann ich eine generated column erstellen, die von `now()` abhängt, z. B. für “inaktiv”?

Nein. Gespeicherte generierte Spalten müssen auf unveränderlichen Ausdrücken basieren; Funktionen wie now() sind in der Regel nicht erlaubt und würden außerdem veraltete Werte erzeugen. Für zeitbasierte Flags wie “90 Tage inaktiv” nutze besser eine normale Spalte, die durch einen Job gepflegt wird, oder berechne es zur Abfragezeit, wenn es kaum genutzt wird.

Was passiert, wenn ich später die Formel einer generated column ändern muss?

Ja, aber behandle es wie eine normale Migration. Eine Änderung des Ausdrucks erfordert ein Schema-Update und ein Recomputen der Werte für bestehende Zeilen, was bei großen Tabellen Zeit und Schreiblast kostet. Plane solche Änderungen kontrolliert.

Fügen generated columns Overhead bei Inserts und Updates hinzu?

Ja. Die Datenbank muss den Wert bei jedem Insert und Update berechnen und speichern. Bei großen Schreiblasten (Imports, Synchronisationen) können zu viele oder zu komplexe generated columns Inserts/Updates verlangsamen. Halte Ausdrücke kurz, füge nur genutzte Felder hinzu und messe die Schreibleistung.

Was ist der sicherste Weg, generierte Spalten für eine bestehende Admin-Liste einzuführen?

Füge die generierte Spalte hinzu, prüfe ein paar reale Zeilen, erstelle dann den Index, der den Hauptfilter und die Hauptsortierung abdeckt, wechsle die Admin-Abfrage zur neuen Spalte und vergleiche Abfragezeit und gescannte Zeilen vor und nach der Änderung, um den Nutzen zu bestätigen.

Einfach zu starten
Erschaffe etwas Erstaunliches

Experimentieren Sie mit AppMaster mit kostenlosem Plan.
Wenn Sie fertig sind, können Sie das richtige Abonnement auswählen.

Starten