Entscheidung beim Datenmodell

JSONB oder echte Spalten: wo die Grenze verläuft

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
Seit 1997 am Markt Kleine Gruppen Präsenz und Live-Online Zertifizierte Trainer
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 Verbindungsarten. 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
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.

Wie gut lässt sich gezielt filtern?

JSONB-Spalte

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.

Eigene Spalte

Gleichheit, Bereiche, Sortierung und Verbindungen laufen über den passenden Index, und der Planer kennt die Verteilung aus der Statistik.

Dokument plus generierte Spalte

Der herausgezogene Wert verhält sich wie jede andere Spalte, inklusive Statistik und aller Indexarten.

Wie gut lässt sich Datenqualität erzwingen?

JSONB-Spalte

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.

Eigene Spalte

Typ, NOT NULL, CHECK und Fremdschlüssel greifen unabhängig davon, welcher Weg die Daten schreibt.

Dokument plus generierte Spalte

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?

JSONB-Spalte

Ein neuer Schlüssel braucht keine Migration und keine Absprache mit dem Betrieb. Genau deshalb entstehen solche Spalten überhaupt.

Eigene Spalte

ALTER TABLE ADD COLUMN läuft in aktuellen Versionen sofort durch, es bleibt aber eine Migration, die geplant und ausgerollt werden muss.

Dokument plus generierte Spalte

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?

JSONB-Spalte

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.

Eigene Spalte

Auch hier wird die ganze Zeile neu geschrieben, sie ist aber schmaler, und große Werte in getrennter Ablage bleiben unberührt.

Dokument plus generierte Spalte

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?

JSONB-Spalte

Für Bedingungen auf Dokumentinhalte gibt es keine Statistik je Schlüssel. Die Schätzungen sind grob, und daraus entstehen ungünstige Verbindungsreihenfolgen.

Eigene Spalte

Häufigkeiten und Histogramme stehen zur Verfügung, die Schätzung ist belastbar und der Plan entsprechend stabil.

Dokument plus generierte Spalte

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.

Fünf Fragen je Attribut

  1. 01 Wird nach dem Attribut gefiltert, sortiert oder gruppiert?
  2. 02 Soll für das Attribut ein Typ oder eine Regel erzwungen werden?
  3. 03 Verweist es auf eine andere Tabelle, braucht also einen Fremdschlüssel?
  4. 04 Ändert es sich häufig, während das übrige Dokument groß ist?
  5. 05 Ist die Struktur je Datensatz wirklich unterschiedlich?
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.

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 schon nicht danebenschä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 Verbindungen. Schätzt der Planer 30 Treffer, wo tatsächlich 300.000 kommen, wählt er eine verschachtelte Schleife, und die Abfrage läuft statt in Millisekunden in Minuten. Genau deshalb ist ein häufig gefiltertes Attribut in einer eigenen Spalte nicht nur schöner, sondern schneller auf eine Weise, die sich durch keinen Index reparieren lässt.

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 Auslöser pflegen.

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. Bei großen Tabellen gehört das in ein Wartungsfenster oder in ein Werkzeug, das die Tabelle im laufenden Betrieb neu aufbaut.

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.

Wissen prüfen

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.

Super Schulung, in der der Trainer auch auf eigene Fragestellungen eingegangen ist.
PostgreSQL Datenbankentwicklung Aufbaukurs
Super Grundlagenschulung mit allen wichtigen Punkten und hilfreichen Tipps!
PostgreSQL Einführung und SQL Grundlagenkurs
Schön, sich 5 Tage ungestört mit Postgres beschäftigen zu können.
PostgreSQL für Administratoren Grundkurs

Häufige Fragen

Ist JSONB langsamer als eine normale Spalte?
Beim reinen Lesen eines Schlüssels ist der Unterschied klein. Deutlich wird er beim Filtern über viele Zeilen, weil die Schätzung des Planers ungenau ist, und beim Schreiben, weil jede Änderung das gesamte Dokument neu schreibt. Bei großen Dokumenten kommt der Aufwand für die getrennte Ablage großer Werte dazu.
Kann ich in JSONB Pflichtfelder erzwingen?
Nur über den Umweg einer CHECK-Bedingung auf einen Ausdruck, etwa CHECK ((daten->>'kundennummer') IS NOT NULL). Das funktioniert für einzelne Schlüssel, wird aber schnell unübersichtlich. Wenn du für ein Attribut Regeln brauchst, ist das ein starkes Zeichen dafür, dass es eine eigene Spalte sein sollte.
Wann lohnt sich ein GIN-Index wirklich?
Wenn du mit @> nach Teilstrukturen suchst oder nicht im Voraus weißt, nach welchem Schlüssel gefiltert wird. Steht der Schlüssel fest, ist ein B-Tree über den Ausdruck kleiner, schneller und pflegeleichter. Ein GIN-Index, der nie benutzt wird, kostet bei jedem Schreibvorgang trotzdem Zeit.
Ersetzt JSONB eine dokumentorientierte Datenbank?
Für viele Anwendungsfälle ja, weil du Dokumente speichern und gleichzeitig relational verknüpfen und Transaktionen nutzen kannst. Was du dir nicht einhandeln solltest, ist ein Modell, das ausschließlich aus Dokumenten besteht: Dann verzichtest du auf genau die Stärken, wegen derer du eine relationale Datenbank einsetzt.
Persönlich für dich da

Deine Ansprechpartner

Du bist dir nicht sicher, welcher Kurs oder welches Level zu dir passt? Wir beraten dich persönlich und kostenlos.

Yves Hoppe

Yves Hoppe

Weiterbildung & Beratung

Hilft dir, aus dem Datenbanken-Programm den passenden Kurs für deinen Stand zu finden.

Norbert Jansen

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.