JSONB: Der beste Weg, Ihre Datenbank zu ruinieren
JSONB ist mächtig und praktisch, wird aber leicht missbraucht, wenn ein Blob zum eigentlichen Schema wird.
PostgreSQL hat JSONB eingeführt, um halbstrukturierte Daten zu speichern, ohne im Vorfeld starre Schemata definieren zu müssen. Die Idee war solide: Manchmal weiß man wirklich nicht, wie die Daten aussehen werden, oder sie ändern sich zu häufig, als dass herkömmliche Spalten sinnvoll wären.
Das ist wichtig, weil JSONB kein Fehlgriff ist. In vielen Systemen ist es die sauberste Abbildung des Problemraums. Wenn Sie Drittanbieter‑Webhook‑Payloads, versionierte Ereigniskörper, Feature‑Flags oder LLM‑Konfigurationsobjekte speichern, bei denen jeder Anbieter und jedes Modell leicht unterschiedliche und ständig wechselnde Optionssätze bereitstellt, kann das Zwingen aller Daten in erstklassige Spalten eher hinderlich als hilfreich sein.
Das Problem ist, dass JSONB zugleich der einfachste Weg ist, Schema‑Entscheidungen aufzuschieben, ohne zuzugeben, dass man sie aufschiebt. Irgendwo zwischen Absicht und Umsetzung wurde es zum Datenbank‑Äquivalent von „Ich räume mein Zimmer später auf.“ Die temporäre Lösung, die Sie vor sechs Monaten gewählt haben? Sie ist immer noch da, und jetzt hängt die Produktion davon ab.
Ich sehe immer wieder dasselbe Muster. Ein Team fügt eine JSONB‑Spalte hinzu, weil die Anforderungen unklar sind. Sie versprechen sich selbst, sie zu normalisieren, sobald sich die Dinge beruhigt haben. Drei Jahre später enthält diese Spalte vierzig verschiedene Versionen dessen, was eigentlich ein Benutzerprofil sein sollte, abgefragt von fünfzehn Services, die jeweils unterschiedliche Annahmen darüber treffen, was darin steckt.
Die technische Schuld liegt nicht am JSONB selbst. Sie liegt in der Lücke zwischen dem, was Sie sich selbst gesagt haben, dass Sie bauen, und dem, was Sie tatsächlich gebaut haben: ein undokumentiertes Schema‑on‑Read‑System.
Was normalerweise passiert
Du fügst ein Feature hinzu und bist dir nicht sicher, ob die Nutzer einen twitter_handle oder einen bluesky_handle oder etwas völlig anderes benötigen. Anstatt das Schema zu durchdenken, machst du Folgendes:
CREATE TABLE users ( id SERIAL PRIMARY KEY, profile JSONB);Es funktioniert. Du lieferst das Feature aus, gehst zum nächsten über und dann zum nächsten. Die JSONB‑Spalte wächst still im Hintergrund.
Das ist die Gabelung. Bleibt profile ein undurchsichtiger Blob, der nur über user.id abgerufen wird, bist du wahrscheinlich in Ordnung. Wird er jedoch zum primären Ort, an dem Geschäftsdaten leben, ändern sich die Kompromisse schnell.
Produkt fragt: „Wie viele Nutzer sind in New York?“
Du schreibst:
SELECT count(*) FROM users WHERE profile->>'location' = 'New York';Postgres führt einen vollständigen Tabellenscan durch – jede einzelne Zeile.
So fügst du einen GIN‑Index hinzu. Vielleicht ist das noch akzeptabel. Manchmal ist es das auch. Aber jetzt zahlst du echte Komplexität und Speicher‑Kosten, weil ein Feld, das sich wie erstklassige relationale Daten verhält, nie zu einer erstklassigen Spalte geworden ist.
Jahr 1: Schema‑Drift
Du hast drei Versionen von Daten in derselben Spalte.
- Zeile 1:
{"city": "NYC"} - Zeile 1000:
{"location": "NYC"} - Zeile 5000:
{"address": {"city": "New York"}}
Dein Anwendungscode sieht jetzt so aus:
const city = user.location || user.city || user.address?.city || "Unknown";Du hast das Schema nicht entfernt. Du hast nur Validierungs‑ und Konsistenzprüfungen von der Datenbank in verstreuten Anwendungscode verlagert.
Wann JSONB wirklich sinnvoll ist
JSONB hat legitime Anwendungsfälle. Oft ist es völlig in Ordnung, und manchmal ist es die beste verfügbare Option.
Die entscheidende Unterscheidung ist nicht „strukturierte Daten gut, JSON schlecht“. Sie liegt eher so:
- Werden die Daten meist als Ganzes über einen stabilen Primärschlüssel abgerufen?
- Variieren die Schlüssel wesentlich zwischen Anbietern, Versionen, Mandanten oder im Zeitverlauf?
- Abfragen Sie nur wenige bekannte Felder, oder erfinden Sie in jedem Sprint neue Pfad‑Abfragen?
- Steuert die Anwendung bewusst Versionierung und Validierung, oder wird das Ganze ad‑hoc gehandhabt?
Legitime JSONB‑Anwendungsfälle
-
Webhook‑Payloads: Sie erhalten Daten von Stripe, Slack oder GitHub. Sie haben keinerlei Kontrolle über das Schema. Sie werden wahrscheinlich nie danach abfragen. Sie müssen sie nur zum Debuggen oder Wiederholen speichern. Perfekt für JSONB.
-
Logging & Event‑Streams: Anwendungs‑Logs, Audit‑Trails, Fehler‑Kontexte. Diese sind schreibintensiv, werden selten nach einzelnen Feldern abgefragt und meist in großen Mengen analysiert oder in Analyse‑Plattformen exportiert. JSONB ist hier in Ordnung.
-
Benutzer‑Präferenzen & Einstellungen: Einstellungs‑Objekte mit 100 + booleschen Flags, von denen die meisten
falsesind, und bei denen Sie immer den gesamten Blob per Benutzer‑ID holen. Sie führen keinWHERE preferences->>'theme' = 'dark'aus. JSONB funktioniert. -
LLM‑Provider / Modell‑Konfiguration: Das ist eines der klarsten modernen Beispiele. OpenAI, Anthropic, Gemini, Open‑Weight‑Lokalmodelle und anbieterspezifische Gateways stellen überlappende, aber unterschiedliche Parameter bereit. Selbst innerhalb eines Anbieters ändern sich Modell‑Fähigkeiten und Optionsnamen. Ein JSONB‑Konfigurations‑Blob ist oft viel ehrlicher, als vorzugeben, dass
temperature,top_p,reasoning_effort,json_schema,tool_choiceund zwanzig weitere Regler universelle Spalten sein sollten. JSONB ist hier häufig die richtige Abstraktion. -
API‑Antwort‑Caching: Sie cachen komplette API‑Antworten. Die Datenbank dient nur als schnelleres Redis. Sie holen nach Cache‑Schlüssel, nie nach verschachtelten Eigenschaften. JSONB ist passend.
-
Event Sourcing: Sie speichern unveränderliche Event‑Payloads. Ihre Abfragen lauten immer „gib mir alle Events für Aggregat X“ sortiert nach Zeit. Sie führen nie
WHERE‑Klauseln auf Event‑Eigenschaften aus. JSONB passt. -
Erweiterbare Oberflächen: Integrationen, Plugin‑Einstellungen, mandantenbezogene Overrides, Marktplatz‑Metadaten, Anbieter‑Fähigkeiten oder „Extras“-Felder, bei denen Sie bewusst erwarten, dass die Struktur je nach Subtyp variiert. JSONB kann der richtige Vertrag sein, nicht ein Kompromiss.
Faustregel: Wenn die Anwendung das Dokument über einen bekannten Schlüssel abruft und weiß, wie es zu validieren bzw. zu versionieren ist, kann JSONB ausgezeichnet sein. Wenn das Business ständig relationale Fragen zu verschachtelten Schlüsseln stellt, versuchen diese Felder, zu Spalten zu werden.
Das beste Muster ist oft hybrid
Viele ausgereifte Systeme landen hier:
CREATE TABLE llm_requests ( id UUID PRIMARY KEY, provider TEXT NOT NULL, model TEXT NOT NULL, status TEXT NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), config JSONB NOT NULL);Das ist in der Regel besser als jedes Extrem.
provider,model,statusundcreated_atsind erstklassige Spalten, weil Sie darauf filtern, joinen, aggregieren und indizieren werden.configbleibt JSONB, weil die genaue Optionsfläche modell‑spezifisch, anbieter‑spezifisch ist und sich wahrscheinlich weiterentwickelt.
Das ist nicht „fehlende Normalisierung“. Das ist das Setzen der Grenze an der richtigen Stelle.
In großem Maßstab: Objektversionierung > Normalisierung
Hier wird es interessant. Bei wirklich großen Datenmengen ist die „richtige“ Lösung nicht die Normalisierung – sondern die Objektversionierung.
Wenn Sie Milliarden von Zeilen und häufige Schema‑Evolution haben, werden Migrationen von Spalten teuer. Unternehmen wie Stripe, GitHub und Netflix normalisieren nicht alles. Stattdessen:
CREATE TABLE entities ( id UUID PRIMARY KEY, version INT NOT NULL, data JSONB NOT NULL);Ihre Anwendung weiß, wie sie version: 1, version: 2, version: 3 liest. Keine Datenbank‑Migrationen für neue Felder. Der Code übernimmt die Abwärtskompatibilität.
Das ist eine architektonische Entscheidung, keine Faulheit. Sie tauscht Datenbankkomplexität gegen Anwendungskomplexität aus. Manchmal ist genau das der richtige Kompromiss, besonders wenn das Dokument von Natur aus versioniert ist und die Anwendung der kanonische Interpreter ist.
Der Fehlermodus ist nicht „JSONB verwenden“. Der Fehlermodus ist, JSONB ohne Versionierung, Validierung, Promotionsregeln oder eine klare Trennung zwischen Dokumentdaten und relationalen Daten zu nutzen.
Die Fragen, die wirklich zählen
Bevor Sie eine JSONB‑Spalte hinzufügen, fragen Sie:
- Werden wir verschachtelte Felder regelmäßig in
WHERE,JOIN,GROUP BYoderORDER BYabfragen? - Kontrollieren wir dieses Schema, oder ist es extern definiert und volatil?
- Ist die Struktur bewusst heterogen über die Datensätze hinweg?
- Haben wir Validierung und Versionierung auf Anwendungsebene?
- Welche Felder werden wahrscheinlich später zu operativen Dimensionen werden?
Wenn die Antwort auf #1 „ja, ständig“ lautet, ist das ein starkes Signal für Spalten.
Wenn die Antworten auf #2 und #3 „ja“ sind, leistet JSONB wahrscheinlich echte Arbeit für Sie.
Der Ausweg aus der Falle
Wenn Sie bereits in diesem Loch stecken, hören Sie auf zu graben.
- Audit: Führen Sie
jsonb_object_keysaus und untersuchen Sie den tatsächlichen Form‑Drift, nicht die Form, die Sie annehmen. - Promote: Identifizieren Sie die Felder, nach denen Sie am häufigsten filtern, joinen, sortieren oder berichten. Machen Sie diese zu echten Spalten.
- Validate: Fügen Sie Anwendungs‑ oder Datenbank‑Validierung für alles hinzu, was im JSONB verbleibt.
- Version: Handelt es sich bei dem Blob um echte Domänendaten, versionieren Sie ihn explizit.
- Trim: Entfernen Sie duplizierte Schlüssel aus dem Blob, sobald die promoted Spalten etabliert sind.
Sagen Sie sich nicht, jedes Blob müsse normalisiert werden. Und sagen Sie sich auch nicht, ein Blob mit permanenten Geschäftssemantiken sei „temporär“.
JSONB ist großartig, wenn das Dokument wirklich dokumenten‑shaped ist. Es wird gefährlich, wenn es ein relationales Schema mit falscher Schnurrbart‑Maske ist.
Ressourcen
- PostgreSQL JSONB Dokumentation
- JSONB Indexierungsstrategien
- Wann JSONB vs relationale Spalten verwenden
- PostgreSQL Schema‑Design Best Practices