JSONB oder eine echte Spalte: die Grenze im Datenmodell
Ein Dokumentfeld kostet nichts, solange niemand darin sucht, und wird teuer in dem Moment, in dem ein Bericht darauf filtert und der Planer die Trefferzahl nicht mehr schätzen kann.
KI-generiertDieses Bild wurde mit KI erzeugt · Yves Hoppe / KI / cmt
Kurz gesagt
Als Faustregel gilt: Attribute, nach denen du filterst, sortierst oder verknüpfst und für die du Regeln erzwingen willst, gehören in eigene Spalten mit Typ, Constraint und Index. JSONB lohnt sich für Zusatzangaben, deren Struktur je Datensatz oder je Mandant unterschiedlich ist und die selten einzeln abgefragt werden. Es gibt einen dritten Weg: das Dokument behalten und die zwei, drei wichtigen Werte als generierte Spalten herausziehen.
Stand dieser Seite: 04.10.2026
01 Worum es geht
Aus dem flexiblen Zusatzfeld wird die halbe Tabelle
Der Anfang ist immer vernünftig. Ein Formular bekommt drei optionale Angaben, die je Mandant anders heißen. Statt drei Spalten anzulegen, die bei den meisten Datensätzen leer bleiben, entsteht eine Spalte attribute vom Typ jsonb. Das spart eine Migration und funktioniert.
Zwei Jahre später stehen dreißig Schlüssel darin. Der Bericht filtert nach attribute->>'region', die Fachabteilung möchte nach attribute->>'kostenstelle' gruppieren, und irgendwo in der Anwendung steht Code, der prüft, ob das Feld betrag eine Zahl oder eine Zeichenkette enthält, weil beides vorkommt. Genau dieser Prüfcode ist die Rechnung für die eingesparte Migration.
Technisch kommen zwei Effekte dazu. Der Planer schätzt die Trefferzahl bei Bedingungen auf Dokumentinhalte deutlich schlechter als bei normalen Spalten, weil ihm die Verteilung einzelner Schlüssel fehlt, und wählt darauf aufbauend ungünstige Join-Methoden. Und jede Änderung an einem einzelnen Schlüssel schreibt das gesamte Dokument neu, weil eine Zeile immer nur als Ganzes neu geschrieben wird.
KI-generiertDieses Bild wurde mit KI erzeugt · Yves Hoppe / KI / cmt
02 Der direkte Vergleich
Dokument, Spalte und der Mittelweg dazwischen
JSONB-Spalte
Ein Dokument je Zeile, beliebige Struktur, keine Migration bei neuen Attributen, indizierbar über GIN oder über Ausdrücke.
Eigene Spalte
Typisiert, mit NOT NULL, CHECK und Fremdschlüssel, sauber geschätzt vom Planer, jede Erweiterung braucht eine Migration.
Dokument plus generierte Spalte
Das Dokument bleibt, die wichtigen Werte werden als gespeicherte generierte Spalten herausgezogen und wie echte Spalten behandelt.
| Entscheidungsfrage | JSONB-Spalte | Eigene Spalte | Dokument plus generierte Spalte |
|---|---|---|---|
| Wie gut lässt sich gezielt filtern? | Kommt darauf an Mit einem GIN-Index geht die Suche nach Enthaltensein schnell, für Bereichsabfragen und Sortierung fehlt die passende Struktur, und die Schätzungen bleiben grob. | Stärke Gleichheit, Bereiche, Sortierung und Joins laufen über den passenden Index, und der Planer kennt die Verteilung aus der Statistik. | Stärke Der herausgezogene Wert verhält sich wie jede andere Spalte, inklusive Statistik und aller Indexarten. |
| Wie gut lässt sich Datenqualität erzwingen? | Schwäche Typ und Pflichtfeld gelten nicht je Schlüssel. Prüfungen musst du in die Anwendung legen oder über CHECK-Bedingungen auf einzelne Ausdrücke nachbilden. | Stärke Typ, NOT NULL, CHECK und Fremdschlüssel greifen unabhängig davon, welcher Weg die Daten schreibt. | Kommt darauf an Auf die generierte Spalte kannst du CHECK legen und einen Fremdschlüssel setzen, das darunterliegende Dokument bleibt aber weiter frei beschreibbar. |
| Was kostet eine neue Angabe im Modell? | Stärke Ein neuer Schlüssel braucht keine Migration und keine Absprache mit dem Betrieb. Genau deshalb entstehen solche Spalten überhaupt. | Kommt darauf an ALTER TABLE ADD COLUMN läuft in aktuellen Versionen sofort durch, es bleibt aber eine Migration, die geplant und ausgerollt werden muss. | Kommt darauf an Für einen neuen Schlüssel entsteht zunächst kein Aufwand. Sobald er wichtig wird, kommt eine generierte Spalte dazu, und das ist wieder eine Migration. |
| Wie verhält sich die Schreiblast? | Schwäche Jede Änderung schreibt das gesamte Dokument neu. Bei großen Dokumenten mit einem häufig geänderten Schlüssel entsteht spürbarer Zusatzaufwand und mehr aufzuräumender Müll. | Kommt darauf an Auch hier wird die ganze Zeile neu geschrieben, sie ist aber schmaler, und große Werte, die per TOAST ausgelagert sind, bleiben unberührt. | Schwäche Die Schreiblast entspricht der des Dokuments, denn geändert wird weiterhin das Dokument. Der Gewinn liegt beim Lesen, nicht beim Schreiben. |
| Wie gut versteht der Planer die Daten? | Schwäche Für Bedingungen auf Dokumentinhalte gibt es keine Statistik je Schlüssel. Die Schätzungen sind grob, und daraus entstehen ungünstige Join-Reihenfolgen. | Stärke Häufigkeiten und Histogramme stehen zur Verfügung, die Schätzung ist belastbar und der Plan entsprechend stabil. | Stärke Für die generierte Spalte werden Statistiken wie für jede andere gesammelt, damit stimmen auch die Schätzungen. |
Wie gut lässt sich gezielt filtern?
Mit einem GIN-Index geht die Suche nach Enthaltensein schnell, für Bereichsabfragen und Sortierung fehlt die passende Struktur, und die Schätzungen bleiben grob.
Gleichheit, Bereiche, Sortierung und Joins laufen über den passenden Index, und der Planer kennt die Verteilung aus der Statistik.
Der herausgezogene Wert verhält sich wie jede andere Spalte, inklusive Statistik und aller Indexarten.
Wie gut lässt sich Datenqualität erzwingen?
Typ und Pflichtfeld gelten nicht je Schlüssel. Prüfungen musst du in die Anwendung legen oder über CHECK-Bedingungen auf einzelne Ausdrücke nachbilden.
Typ, NOT NULL, CHECK und Fremdschlüssel greifen unabhängig davon, welcher Weg die Daten schreibt.
Auf die generierte Spalte kannst du CHECK legen und einen Fremdschlüssel setzen, das darunterliegende Dokument bleibt aber weiter frei beschreibbar.
Was kostet eine neue Angabe im Modell?
Ein neuer Schlüssel braucht keine Migration und keine Absprache mit dem Betrieb. Genau deshalb entstehen solche Spalten überhaupt.
ALTER TABLE ADD COLUMN läuft in aktuellen Versionen sofort durch, es bleibt aber eine Migration, die geplant und ausgerollt werden muss.
Für einen neuen Schlüssel entsteht zunächst kein Aufwand. Sobald er wichtig wird, kommt eine generierte Spalte dazu, und das ist wieder eine Migration.
Wie verhält sich die Schreiblast?
Jede Änderung schreibt das gesamte Dokument neu. Bei großen Dokumenten mit einem häufig geänderten Schlüssel entsteht spürbarer Zusatzaufwand und mehr aufzuräumender Müll.
Auch hier wird die ganze Zeile neu geschrieben, sie ist aber schmaler, und große Werte, die per TOAST ausgelagert sind, bleiben unberührt.
Die Schreiblast entspricht der des Dokuments, denn geändert wird weiterhin das Dokument. Der Gewinn liegt beim Lesen, nicht beim Schreiben.
Wie gut versteht der Planer die Daten?
Für Bedingungen auf Dokumentinhalte gibt es keine Statistik je Schlüssel. Die Schätzungen sind grob, und daraus entstehen ungünstige Join-Reihenfolgen.
Häufigkeiten und Histogramme stehen zur Verfügung, die Schätzung ist belastbar und der Plan entsprechend stabil.
Für die generierte Spalte werden Statistiken wie für jede andere gesammelt, damit stimmen auch die Schätzungen.
Was passt wann
- Ein Attribut taucht in Berichten, Filtern oder Verknüpfungen auf.
- Eigene Spalte, mindestens aber eine gespeicherte generierte Spalte über dem Dokument.
- Jeder Mandant liefert andere Zusatzfelder, und sie werden fast nur zusammen mit dem Datensatz angezeigt.
- JSONB, mit einem GIN-Index nur dann, wenn tatsächlich darin gesucht wird.
- Das Modell steht noch nicht fest und die Anforderungen ändern sich wöchentlich.
- Mit JSONB starten, aber von Anfang an beobachten, welche Schlüssel in Abfragen auftauchen. Diese wandern nach und nach in Spalten.
03 Was du mitnimmst
Was du nach dieser Seite entscheiden kannst
Die Frage lautet nicht, ob JSONB gut oder schlecht ist, sondern welche Rolle ein Attribut in der Anwendung spielt. Diese Rolle lässt sich für jeden Schlüssel in einem Satz beantworten, und daraus ergibt sich die Zuordnung fast von selbst.
Fünf Fragen je Attribut
- 01 Wird nach dem Attribut gefiltert, sortiert oder gruppiert?
- 02 Soll für das Attribut ein Typ oder eine Regel erzwungen werden?
- 03 Verweist es auf eine andere Tabelle, braucht also einen Fremdschlüssel?
- 04 Ändert es sich häufig, während das übrige Dokument groß ist?
- 05 Ist die Struktur je Datensatz wirklich unterschiedlich?
Die Filterfrage stellen
Steht ein Attribut jemals in einer WHERE-Bedingung, in einem JOIN oder in einer Gruppierung, gehört es in eine eigene Spalte. Alles andere ist eine Wette darauf, dass der Planer sich schon nicht verschätzen wird.
Datenqualität einfordern
Eine echte Spalte kennt Typ, NOT NULL, CHECK und Fremdschlüssel. In JSONB ist jeder Wert erst einmal erlaubt, auch die Zahl als Zeichenkette und der Tippfehler im Schlüsselnamen. Die Prüfung wandert dann in die Anwendung, und zwar in jede.
Die Indexart bewusst wählen
Ein GIN-Index über die ganze Spalte bedient Enthaltensein und Schlüsselsuche, wird aber groß. Ein B-Tree über einen Ausdruck wie ((daten->>'region')) ist klein und schnell für Gleichheit auf genau diesem einen Schlüssel.
Schreiblast realistisch einschätzen
Wird ein einzelner Schlüssel häufig aktualisiert, während das restliche Dokument groß ist, schreibt die Datenbank bei jeder Änderung viele unnötige Bytes. Genau dieser Schlüssel gehört als Spalte heraus.
Generierte Spalten als Mittelweg nutzen
Eine gespeicherte generierte Spalte zieht einen Wert aus dem Dokument, ist typisiert, indizierbar und bleibt automatisch synchron. Du behältst das Dokument und bekommst für die wichtigen Attribute trotzdem echte Spalten.
json und jsonb auseinanderhalten
json speichert den Text unverändert samt Leerzeichen und doppelten Schlüsseln und muss bei jedem Zugriff neu geparst werden. jsonb speichert zerlegt, ist indizierbar und die richtige Wahl, sobald du mit den Daten arbeiten und sie nicht nur archivieren willst.
KI-generiertDieses Bild wurde mit KI erzeugt · Yves Hoppe / KI / cmt
json, jsonb und was der Unterschied im Alltag bedeutet
json speichert genau die Zeichenkette, die hereinkommt, inklusive Leerzeichen, Reihenfolge der Schlüssel und doppelt vorkommender Schlüssel, und wird bei jedem Zugriff neu geparst. jsonb speichert eine zerlegte Form: Die Reihenfolge geht verloren, doppelte Schlüssel werden auf den letzten reduziert, dafür ist der Zugriff auf einen Schlüssel günstig und die Spalte lässt sich indizieren.
Für Anwendungsdaten ist jsonb praktisch immer die richtige Wahl. json ist dann sinnvoll, wenn du eine eingegangene Nachricht unverändert aufbewahren musst, etwa für eine spätere Prüfung oder eine Signatur, und sie ohnehin nicht durchsuchst.
Die Zugriffsoperatoren verwechselt man am Anfang zuverlässig. -> liefert wieder jsonb, ->> liefert Text. Ein Vergleich wie daten->'menge' = '5' vergleicht deshalb JSON-Werte, während (daten->>'menge')::int = 5 das tut, was gemeint war. Für Pfade gibt es #> und #>>, und für komplexere Ausdrücke die SQL/JSON-Pfadsprache mit den Operatoren @? und @@.
Indizieren, ohne die Datenbank aufzublähen
Der voreingestellte GIN-Index über eine jsonb-Spalte legt Einträge für Schlüssel und Werte an. Er bedient sowohl den Enthaltensein-Operator @> als auch die Frage, ob ein Schlüssel existiert, und wird entsprechend groß. Die Variante jsonb_path_ops indiziert nur Kombinationen aus Pfad und Wert als Hashwerte, ist deutlich kleiner und für @> meist schneller, unterstützt dafür die Existenzfrage nicht.
In vielen Fällen brauchst du gar keinen GIN-Index. Wenn nur nach einem einzigen Schlüssel gefiltert wird, ist ein B-Tree über den Ausdruck die bessere Wahl: CREATE INDEX ON auftraege (((daten->>'region'))). Der Index bleibt klein, unterstützt Bereiche und Sortierung, und die Abfrage muss den Ausdruck exakt so schreiben, damit er verwendet wird.
Teilindizes passen gut dazu. Wird nur in den offenen Vorgängen gesucht, indiziere über eine WHERE-Bedingung am Index auch nur diese. Und beachte, dass ein Index über einen Ausdruck eigene Statistiken bekommt, was der Schätzung zusätzlich hilft.
Warum Schätzungen bei Dokumenten danebenliegen
Der Planer entscheidet anhand geschätzter Trefferzahlen, ob er einen Index nutzt und in welcher Reihenfolge er Tabellen verbindet. Für normale Spalten stützt er sich auf gesammelte Statistiken mit häufigsten Werten und Histogrammen. Für eine Bedingung auf einen Schlüssel innerhalb eines Dokuments gibt es diese Verteilung nicht, er arbeitet dann mit pauschalen Annahmen.
Die Folge sieht man erst bei Joins. Schätzt der Planer 30 Treffer, wo tatsächlich 300.000 kommen, wählt er einen Nested Loop, und die Abfrage läuft statt in Millisekunden in Minuten. Ein GIN-Index ändert an dieser Schätzung nichts. Abhilfe schaffen nur Statistiken auf genau den Ausdruck: Ein Index über den Ausdruck bringt sie mit, ab PostgreSQL 14 auch CREATE STATISTICS auf einen Ausdruck. Ein häufig gefiltertes Attribut in einer eigenen Spalte hat diese Statistik von selbst und ist deshalb nicht nur schöner, sondern auch planbar.
Eine gespeicherte generierte Spalte löst das sauber, weil sie normal analysiert wird. Der Ausdruck muss dafür unveränderlich sein, ein Ausdruck wie ((daten->>'menge')::int) erfüllt das. Die Spalte bleibt automatisch synchron zum Dokument, du musst also keine Trigger pflegen. Achte seit PostgreSQL 18 auf das Schlüsselwort STORED: Ohne Angabe legt die Datenbank eine virtuelle generierte Spalte an, die bei jedem Lesen berechnet wird und sich nicht indizieren lässt.
Der Umbau, wenn es schon zu spät ist
Wenn ein Dokumentfeld längst dreißig Schlüssel hat, hilft kein Grundsatzbeschluss, sondern eine Rangfolge. Wertest du das Abfrageprotokoll aus, siehst du, welche Schlüssel überhaupt in Bedingungen auftauchen, und meist sind es drei bis fünf. Genau diese ziehst du heraus, alles andere kann bleiben.
Der Weg ohne Ausfall geht über generierte Spalten. Du legst sie an, passt die Abfragen an, legst die Indizes darauf und lässt das Dokument unverändert. Wird die Anwendung später auch schreibend auf echte Spalten umgestellt, ist der Lesepfad schon fertig, und der Schlüssel kann in einem zweiten Schritt aus dem Dokument verschwinden.
Rechne beim Anlegen mit einer Umschreibung der Tabelle, denn eine gespeicherte generierte Spalte wird für jede vorhandene Zeile berechnet. Die Tabelle bleibt währenddessen exklusiv gesperrt, bei großen Tabellen gehört das also in ein Wartungsfenster. Geht das nicht, legst du stattdessen eine normale Spalte an, füllst sie in Blöcken nach und hältst sie mit einem Trigger synchron.
Dazu passende Kurse
Wie sich solche Entscheidungen im Modell auswirken, machen die PostgreSQL-Schulungen zu Datenmodellierung und JSONB an vorhandenen Datenbeständen sichtbar.
Die Abwägung zwischen Struktur und Freiheit stellt sich in jedem System, deshalb sind Kurse zur Datenmodellierung über Systemgrenzen hinweg auch für gemischte Landschaften interessant.
Wie sicher bist du beim Thema wirklich?
Lesen fühlt sich schnell nach Können an. Ein kurzer Test zeigt dir, was davon schon sitzt und wo sich ein Kurs lohnt. Kostenlos, ohne Anmeldung, mit einer Erklärung zu jeder Antwort.
04 Konkrete Kurse
Wo du genau das übst
- PostgreSQL 2026 Aufbaukurs: Architektur, JSONB, Time-Series
- PostgreSQL Datenbankentwicklung Aufbaukurs
- PostgreSQL Datenbankentwicklung Grundkurs
Weitere Kurse aus diesem Bereich
Preise, Orte und Buchung für alle Kurse des Bereichs findest du im Katalog: Alle Datenbank-Schulungen mit Terminen.
Stimmen aus den Kursen
Was Teilnehmende über die Kurse in diesem Bereich sagen
Super Schulung, in der der Trainer auch auf eigene Fragestellungen eingegangen ist.
Super Grundlagenschulung mit allen wichtigen Punkten und hilfreichen Tipps!
Schön, sich 5 Tage ungestört mit Postgres beschäftigen zu können.
05 Fragen
Häufige Fragen
Deine Frage ist nicht dabei? Stell sie uns direkt, wir antworten dir persönlich.
Ist JSONB langsamer als eine normale Spalte?
Kann ich in JSONB Pflichtfelder erzwingen?
Wann lohnt sich ein GIN-Index wirklich?
Ersetzt JSONB eine dokumentorientierte Datenbank?
Deine Ansprechpartner
Du bist dir nicht sicher, welcher Kurs oder welches Level zu dir passt? Wir beraten dich persönlich und kostenlos.
Yves Hoppe
Weiterbildung & Beratung
Hilft dir, aus dem Datenbanken-Programm den passenden Kurs für deinen Stand zu finden.
Norbert Jansen
Beratung & Inhouse
Plant mit dir Inhouse-Trainings, die auf eure Abläufe und euren Datenbestand zugeschnitten sind.
Modellentscheidungen, die zwei Jahre halten
Wo JSONB trägt und wo eine Spalte hingehört, arbeiten die PostgreSQL-Kurse bei cmt an Modellen durch, die schon gewachsen sind.