JSONB: Der beste Weg, Ihre Datenbank zu ruinieren
JSONB ist mächtig, nützlich und sehr leicht falsch einzusetzen, wenn man einen Blob zur eigentlichen Schema-Instanz werden lässt.
PostgreSQL hat JSONB eingeführt, um semi-strukturierte Daten speichern zu können, ohne von vornherein starre Schemata definieren zu müssen. Die Idee war solide: Manchmal weiß man genuinely nicht, wie die Daten aussehen werden, oder sie ändern sich zu häufig, als dass traditionelle Spalten sinnvoll wären.
Das ist wichtig, weil JSONB kein Fehler ist. In vielen Systemen ist es die sauberste Repräsentation des Problemraums. Wenn Sie Webhook-Payloads von Drittanbietern speichern, versionierte Event-Bodies, Feature-Flags oder LLM-Konfigurationsobjekte, bei denen jeder Provider und jedes Modell einen leicht anderen und sich ständig ändernden Optionssatz exposes, kann das Erzwingen allesamt in erstklassige Spalten umständlicher sein als hilfreich.
Das Problem ist, dass JSONB auch der einfachste Weg ist, Schema-Entscheidungen hinauszuzögern, ohne zuzugeben, dass man sie hinauszögert. Irgendwo zwischen Intention und Implementation wurde es zum Datenbank-Äquivalent von „Ich räume später mein Zimmer auf.” Diese temporäre Lösung, nach der Sie vor sechs Monaten gegriffen 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 es sich bei den Anforderungen unsicher ist. Man verspricht sich selbst, zu normalisieren, sobald sich die Dinge eingependelt haben. Drei Jahre später enthält diese Spalte vierzig verschiedene Versionen dessen, was ein Benutzerprofil sein sollte, abgefragt von fünfzehn Services, die jeweils unterschiedliche Annahmen darüber treffen, was sich darin befindet.
Die technische Schuld ist nicht das JSONB selbst. Sie ist die Lücke zwischen dem, was man sich selbst zu bauen erzählte, und dem, was man tatsächlich baute: ein undokumentiertes Schema-on-Read-System.
Was normalerweise passiert
Sie fügen ein Feature hinzu und sind sich nicht sicher, ob Benutzer einen twitter_handle oder einen bluesky_handle oder etwas ganz anderes brauchen. Anstatt das Schema durchzudenken, machen Sie Folgendes:
CREATE TABLE users ( id SERIAL PRIMARY KEY, profile JSONB);Es funktioniert. Sie shippen das Feature, machen mit dem nächsten weiter, dann mit dem nächsten. Die JSONB-Spalte wächst leise im Hintergrund.
Das ist die Weggabelung. Wenn profile ein undurchsichtiger Blob bleibt, der per user.id abgerufen wird, sind Sie wahrscheinlich fine. Wenn es jedoch zum primären Ort wird, an dem Geschäftsdaten leben, ändern sich die Tradeoffs schnell.
Product fragt: „Wie viele Benutzer befinden sich in New York?”
Sie schreiben:
SELECT count(*) FROM users WHERE profile->>'location' = 'New York';PostgreSQL führt einen Full-Table-Scan durch. Jede einzelne Zeile.
Also fügen Sie einen GIN-Index hinzu. Vielleicht ist das immer noch akzeptabel. Manchmal ist es das. Aber jetzt zahlen Sie echte Komplexitäts- und Speicherkosten, weil ein Feld, das sich wie erstklassige relationale Daten verhält, nie zu einer erstklassigen Spalte wurde.
Jahr 1: Schema-Drift
Sie haben drei Versionen von Daten in derselben Spalte.
- Zeile 1:
{"city": "NYC"} - Zeile 1000:
{"location": "NYC"} - Zeile 5000:
{"address": {"city": "New York"}}
Ihr Anwendungscode sieht jetzt so aus:
const city = user.location || user.city || user.address?.city || "Unknown";Sie haben das Schema nicht entfernt. Sie haben Validierung und Konsistenzprüfungen lediglich von der Datenbank in verstreuten Anwendungscode verschoben.
Wann man JSONB tatsächlich verwenden sollte
JSONB hat legitime Anwendungsfälle. Oft ist es vollkommen in Ordnung, und manchmal ist es die beste verfügbare Wahl.
Die kritische Unterscheidung ist nicht „strukturiert gut, JSON schlecht.” Sie liegt näher hier:
- Werden die Daten meistens als Ganzes über einen stabilen Primärschlüssel abgerufen?
- Variieren die Keys material über Provider, Versionen, Mandanten oder Zeit?
- Fragen Sie ein paar bekannte Felder ab oder erfinden Sie jeden Sprint neue Path-Queries?
- Besitzt die Anwendung Versionierung und Validierung intentional, oder wird einfach improvisiert?
Legitime JSONB-Anwendungsfälle
-
Webhook-Payloads: Sie erhalten Daten von Stripe, Slack oder GitHub. Sie haben keine Kontrolle über das Schema. Möglicherweise werden Sie es nie abfragen. Sie müssen es nur zum Debuggen oder Replay speichern. Perfekt für JSONB.
-
Logging & Event-Streams: Anwendungslogs, Audit-Trails, Error-Kontexte. Diese sind schreibintensiv, werden selten nach spezifischen Feldern abgefragt und oft in Bulk analysiert oder in Analytics-Plattformen exportiert. JSONB ist hier in Ordnung.
-
Benutzereinstellungen & Konfigurationen: Settings-Objekte mit 100+ Boolean-Flags, von denen die meisten false sind, und Sie fetchen immer den gesamten Blob per Benutzer-ID. Sie führen nicht
WHERE preferences->>'theme' = 'dark'aus. JSONB funktioniert. -
LLM-Provider-/Modell-Konfiguration: Das ist eines der klarsten modernen Beispiele. OpenAI, Anthropic, Gemini, Open-Weight-Local-Models und vendor-spezifische Gateways expose alle überlappende, aber unterschiedliche Parameter. Selbst innerhalb eines Providers entwickeln sich Modellfähigkeiten und Optionsnamen weiter. Ein JSONB-Konfigurations-Blob ist oft ehrlicher, als so zu tun, als sollten
temperature,top_p,reasoning_effort,json_schema,tool_choiceund zwanzig weitere Knöpfe allesamt universelle Spalten sein. JSONB ist hier oft die richtige Abstraktion. -
API-Response-Caching: Sie cachen gesamte API-Responses. Die Datenbank ist nur ein schnelleres Redis. Sie fetchen per Cache-Key, nie nach verschachtelten Properties. JSONB ist angebracht.
-
Event Sourcing: Sie speichern immutable Event-Payloads. Ihre Queries sind immer „gib mir alle Events für Aggregat X”, sortiert nach Zeit. Sie führen nie
WHERE-Klauseln auf Event-Properties aus. JSONB passt. -
Erweiterbarkeits-Oberflächen: Integrationen, Plugin-Einstellungen, mandantenspezifische Overrides, Marketplace-Metadaten, Provider-Capabilities oder „Extras”-Felder, bei denen Sie explizit erwarten, dass die Form je nach Subtyp variiert. JSONB kann der richtige Vertrag sein, kein Kompromiss.
Rule of Thumb: Wenn die Anwendung das Dokument über einen bekannten Key fetcht und weiß, wie sie es validiert/versioniert, kann JSONB exzellent sein. Wenn das Business jedoch ständig relationale Fragen über verschachtelte Keys stellt, versuchen diese Felder, Spalten zu werden.
Das beste Pattern 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 meist besser als beide Extreme.
provider,model,statusundcreated_atsind erstklassige Spalten, weil Sie sie filtern, joinen, aggregieren und indexieren werden.configbleibt JSONB, weil die genaue Options-Oberfläche modellspezifisch, providerspezifisch ist und sich wahrscheinlich weiterentwickeln wird.
Das ist kein „gescheitertes Normalisieren.” Das ist, die Linie an der richtigen Stelle zu ziehen.
Im großen Maßstab: Object Versioning > Normalisierung
Hier wird es interessant. Bei ausreichend großem Maßstab ist die „richtige” Lösung nicht Normalisierung – sondern Object Versioning.
Wenn Sie Milliarden von Zeilen und häufige Schema-Evolution haben, werden Column-Migrationen 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. Code handhabt Abwärtskompatibilität.
Das ist eine architektonische Entscheidung, keine Faulheit. Es tauscht Datenbank-Komplexität gegen Anwendungs-Komplexität. Manchmal ist das genau der richtige Trade-off, besonders wenn das Dokument natürlich versioniert ist und die App der kanonische Interpreter ist.
Der Failure Mode ist nicht „JSONB verwenden.” Der Failure Mode ist, JSONB ohne Versionierung, Validierung, Promotions-Regeln oder eine klare Grenze zwischen Dokumentdaten und relationalen Daten zu verwenden.
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 Form intentional heterogen über Records hinweg?
- Haben wir Validierung und Versionierung auf Anwendungsebene?
- Welche Felder werden wahrscheinlich später zu operationalen Dimensionen?
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” lauten, leistet JSONB wahrscheinlich echte Arbeit für Sie.
Der Falle entkommen
Wenn Sie bereits in diesem Loch stecken, hören Sie auf zu graben.
- Audit: Führen Sie
jsonb_object_keysaus und inspizieren Sie die tatsächliche Shape-Drift, nicht die Form, von der Sie ausgehen. - Promoten: Identifizieren Sie die Felder, die Sie am häufigsten filtern, joinen, sortieren oder reporten. Machen Sie daraus echte Spalten.
- Validieren: Fügen Sie Validierung auf Anwendungs- oder Datenbankebene für das hinzu, was in JSONB verbleibt.
- Versionieren: Wenn der Blob echte Domänendaten sind, versionieren Sie ihn explizit.
- Trimmen: Entfernen Sie duplizierte Keys aus dem Blob, sobald die promoted Spalten etabliert sind.
Erzählen Sie sich nicht, jeder Blob müsse normalisiert werden. Erzählen Sie sich aber auch nicht, ein Blob mit permanenter Geschäftssemantik sei „temporär.”
JSONB ist großartig, wenn das Dokument genuinely document-shaped ist. Es ist gefährlich, wenn es ein relationales Schema mit einem falschen Schnurrbart ist.
Ressourcen
- PostgreSQL JSONB Documentation
- JSONB Indexing Strategies
- When to Use JSONB vs Relational Columns
- PostgreSQL Schema Design Best Practices