JSON-Spalten in relationalen Datenbanken nutzen
AI generated
SELECT
JOIN
SQL · JSON · PostgreSQL · MySQL
JSON-Spalten in relationalen Datenbanken nutzen
Semi-strukturierte Daten ohne NoSQL-Wechsel

JSON-Spalten geben einer relationalen Datenbank genau die Modellierungsfreiheit, die sonst als Argument fuer einen NoSQL-Wechsel dient, ohne auf Transaktionen, Joins und referentielle Integritaet zu verzichten. Wer JSONB in PostgreSQL oder JSON in MySQL richtig indiziert, abfragt und validiert, deckt viele semi-strukturierte Anwendungsfaelle innerhalb der bestehenden Datenbank ab.

16 Min. Lesezeit JSONB · GIN-Index · JSON-Operatoren · Validierung PostgreSQL 15+ · MySQL 8 · SQL Server 2022

1. Warum JSON-Spalten kein Widerspruch zur relationalen Welt sind

JSON-Spalten loesen ein Problem, das lange als Argument fuer einen kompletten Wechsel zu NoSQL galt: variable, verschachtelte Datenstrukturen innerhalb eines ansonsten stabilen relationalen Schemas. Statt fuer jedes zusaetzliche, selten benoetigte Attribut eine neue Spalte anzulegen oder ein aufwaendiges Entity-Attribute-Value-Modell zu bauen, speichert eine JSON-Spalte diese variablen Teile direkt als strukturiertes Dokument in einer einzigen Zelle, waehrend der Rest der Tabelle normal relational bleibt.

Der entscheidende Vorteil gegenueber einem echten Wechsel zu einer dokumentbasierten NoSQL-Datenbank: JSON-Spalten laufen innerhalb derselben Transaktion wie alle anderen Spalten der Tabelle, profitieren von denselben ACID-Garantien und lassen sich mit normalen SQL-Joins mit anderen Tabellen verbinden. PostgreSQL, MySQL und SQL Server unterstuetzen JSON-Spalten heute alle nativ, mit eigenen Operatoren zum Lesen, Schreiben und Indizieren der enthaltenen Struktur.

Dieser Beitrag zeigt, wie man JSON-Spalten sinnvoll einsetzt, welche Unterschiede zwischen JSON und JSONB bestehen, wie man sie effizient abfragt und indiziert, und wo die Grenzen dieses Ansatzes liegen, bevor ein eigenes Schema oder ein echter NoSQL-Wechsel die bessere Wahl waere.

2. JSON versus JSONB: Speicherformat und Performance

PostgreSQL bietet zwei Datentypen fuer JSON-Spalten: json speichert den Text exakt so, wie er eingegeben wurde, inklusive urspruenglicher Formatierung und Schluesselreihenfolge, validiert aber erst beim Schreiben die Syntax. jsonb speichert dieselben Daten in einem binaeren, dekomponierten Format, das keine Formatierung und keine doppelten Schluessel behaelt, dafuer aber deutlich schneller abzufragen und zu indizieren ist, weil beim Lesen keine erneute Parsing-Runde noetig ist.

Fuer nahezu jeden praktischen Anwendungsfall ist jsonb die richtige Wahl. Der einzige Vorteil von json ist der geringfuegig schnellere Schreibvorgang, weil keine Umwandlung ins binaere Format stattfindet, und die exakte Erhaltung der urspruenglichen Formatierung, was nur relevant ist, wenn man das Dokument spaeter byteidentisch zurueckgeben muss. MySQL kennt nur einen JSON-Typ, der intern bereits binaer und optimiert gespeichert wird, eine Unterscheidung wie bei PostgreSQL entfaellt dort.


-- JSONB als Spaltentyp in PostgreSQL: binaer, indizierbar, schnell abfragbar
CREATE TABLE products (
  product_id   INT PRIMARY KEY,
  name         VARCHAR(255) NOT NULL,
  price        NUMERIC(10,2) NOT NULL,
  attributes   JSONB NOT NULL DEFAULT '{}'::jsonb
);

INSERT INTO products (product_id, name, price, attributes) VALUES
  (1, 'Laufschuh Modell X', 89.90, '{"size": 42, "color": "black", "weight_grams": 310}'),
  (2, 'Bluetooth-Kopfhoerer', 59.90, '{"battery_hours": 24, "bluetooth_version": "5.3"}');

-- json (Text-Variante) behaelt exakte Formatierung, ist aber langsamer bei Abfragen
-- ALTER TABLE products ADD COLUMN legacy_payload JSON;

3. Wann JSON-Spalten sinnvoll sind und wann nicht

JSON-Spalten passen gut zu Attributen, die selten in WHERE-Bedingungen mit komplexen Bereichsvergleichen vorkommen, sich zwischen Zeilen stark unterscheiden, oder deren Struktur sich haeufiger aendert als eine Schema-Migration rechtfertigen wuerde. Typische Beispiele sind Produktattribute in einem heterogenen Katalog, Konfigurationsobjekte fuer Feature-Flags, oder Antwortdaten aus externen APIs, die man archivieren, aber nicht vollstaendig normalisieren will.

Ungeeignet sind JSON-Spalten dagegen fuer Daten, die haeufig mit Aggregatfunktionen ausgewertet, per Fremdschluessel referenziert oder mit strengen Constraints belegt werden muessen. Ein Feld wie customer_id, das in Joins, Fremdschluesseln und Indizes verwendet wird, gehoert immer als eigene, typisierte Spalte in die Tabelle, niemals in ein JSON-Dokument, weil sonst referentielle Integritaet und effiziente Joins verloren gehen. Die Faustregel: stabile, stark abgefragte Kernfelder bleiben klassische Spalten, variable Zusatzattribute wandern in die JSON-Spalte.

4. JSON-Daten abfragen: Operatoren und Pfad-Ausdruecke

PostgreSQL bietet fuer JSON-Spalten mehrere Operatoren: -> extrahiert ein Feld als JSON-Wert, ->> extrahiert dasselbe Feld als Text, und #> beziehungsweise #>> erlauben verschachtelte Pfade ueber ein Array von Schluesseln. Der Containment-Operator @> prueft, ob ein JSON-Dokument ein anderes JSON-Fragment vollstaendig enthaelt, was besonders fuer Filterabfragen ueber mehrere Attribute nuetzlich ist, ohne jedes Feld einzeln extrahieren zu muessen.


-- Feld als Text extrahieren mit ->>
SELECT name, attributes->>'color' AS color
FROM products
WHERE attributes->>'color' = 'black';

-- Verschachtelten Pfad abfragen mit #>>
SELECT name, attributes#>>'{dimensions,height_cm}' AS height
FROM products
WHERE (attributes#>>'{dimensions,height_cm}')::numeric > 20;

-- Containment-Operator: enthaelt attributes dieses Teil-Dokument?
SELECT name FROM products
WHERE attributes @> '{"color": "black", "size": 42}';

-- Existenzpruefung eines Schluessels mit dem ? Operator
SELECT name FROM products WHERE attributes ? 'battery_hours';

5. Indizierung von JSON-Spalten mit GIN und Expression-Indizes

Ohne Index scannt eine Abfrage auf JSON-Spalten jede Zeile vollstaendig, was bei grossen Tabellen inakzeptabel langsam wird. PostgreSQL bietet fuer jsonb-Spalten den GIN-Index, Generalized Inverted Index, der auf beliebige Schluessel-Wert-Paare innerhalb des Dokuments beschleunigt zugreifen kann und damit den @>-Operator und Existenzpruefungen mit ? massiv beschleunigt. Fuer haeufig abgefragte Einzelfelder ist ein Expression-Index auf genau diesem Pfad oft effizienter als ein allgemeiner GIN-Index.


-- GIN-Index fuer allgemeine Containment- und Existenzabfragen
CREATE INDEX idx_products_attributes_gin ON products USING GIN (attributes);

-- Expression-Index fuer ein haeufig gefiltertes Einzelfeld
-- deutlich kompakter und schneller als GIN fuer genau diesen Anwendungsfall
CREATE INDEX idx_products_color ON products ((attributes->>'color'));

-- Test: EXPLAIN zeigt, ob der Index tatsaechlich verwendet wird
EXPLAIN ANALYZE
SELECT name FROM products WHERE attributes->>'color' = 'black';
-- Erwartung: Index Scan statt Seq Scan bei ausreichender Tabellengroesse

6. Validierung und Constraints fuer JSON-Inhalte

Ein haeufiges Missverstaendnis: JSON-Spalten bedeuten nicht automatisch, dass jede beliebige Struktur erlaubt sein muss. CHECK-Constraints koennen sicherstellen, dass Pflichtfelder vorhanden sind, dass Werte einem erwarteten Typ entsprechen, oder dass ein Dokument einer festen Menge erlaubter Schluessel folgt. Damit behaelt man die Flexibilitaet von JSON, ohne vollstaendig auf Datenintegritaet zu verzichten.


-- CHECK-Constraint erzwingt Pflichtfelder in der JSON-Spalte
ALTER TABLE products ADD CONSTRAINT attributes_has_required_keys
  CHECK (attributes ? 'color' AND attributes ? 'weight_grams');

-- Typpruefung eines einzelnen Feldes ueber jsonb_typeof
ALTER TABLE products ADD CONSTRAINT weight_is_number
  CHECK (jsonb_typeof(attributes->'weight_grams') = 'number');

-- Test: verletzt beide Constraints, wird abgelehnt
INSERT INTO products (product_id, name, price, attributes)
VALUES (99, 'Test-Produkt', 10.00, '{"color": "red"}');
-- ERROR: new row for relation "products" violates check constraint
-- "attributes_has_required_keys"

7. Migration: von starren Spalten zu JSON und zurueck

Eine typische Migration beginnt mit mehreren fest definierten Spalten fuer optionale Attribute, die sich als zu starr herausstellen, weil staendig neue, seltene Attribute hinzukommen. Der Uebergang zu JSON-Spalten erfolgt schrittweise: eine neue JSONB-Spalte wird angelegt, bestehende Werte werden per UPDATE in die neue Struktur uebertragen, Anwendungscode wird schrittweise umgestellt, und erst wenn alle Lese- und Schreibpfade migriert sind, werden die alten Spalten entfernt.

Der umgekehrte Weg ist ebenso wichtig: Stellt sich heraus, dass ein bestimmtes JSON-Feld inzwischen in praktisch jeder Zeile vorkommt, stark gefiltert wird und von Constraints profitieren wuerde, lohnt sich die Extraktion in eine eigene, typisierte Spalte. PostgreSQL erlaubt das direkt per generierter Spalte, die den Wert automatisch aus dem JSON extrahiert und gleichzeitig normal indizierbar bleibt.


-- MySQL: haeufig abgefragtes JSON-Feld als generierte Spalte extrahieren
-- STORED materialisiert den Wert physisch und erlaubt einen normalen Index
ALTER TABLE products
  ADD COLUMN color VARCHAR(30)
  GENERATED ALWAYS AS (JSON_UNQUOTE(JSON_EXTRACT(attributes, '$.color'))) STORED,
  ADD INDEX idx_products_color (color);

-- Diese generierte Spalte verhaelt sich wie eine normale Spalte:
-- indizierbar, per Fremdschluessel referenzierbar, in Aggregaten nutzbar,
-- bleibt aber automatisch synchron mit dem JSON-Ursprungsfeld

8. Unterschiede zwischen PostgreSQL, MySQL und SQL Server

Alle drei grossen relationalen Datenbanken unterstuetzen JSON-Spalten, mit unterschiedlicher Tiefe der Funktionalitaet. PostgreSQL mit jsonb bietet die reichhaltigste Operator- und Indexunterstuetzung, inklusive GIN-Indizes und einer eigenen JSON-Path-Sprache ab Version 12. MySQL 8 bietet einen nativen, binaer gespeicherten JSON-Typ mit Funktionen wie JSON_EXTRACT, JSON_SET und generierten Spalten fuer Indizierung, ist aber bei komplexen Pfadabfragen etwas weniger performant als PostgreSQL.

SQL Server speichert JSON dagegen als gewoehnlichen NVARCHAR-Text und bietet Funktionen wie JSON_VALUE und JSON_QUERY zum Extrahieren, ohne einen dedizierten binaeren JSON-Typ. Indizierung erfolgt dort ueber berechnete Spalten in Kombination mit normalen Indizes, aehnlich dem MySQL-Ansatz mit generierten Spalten. Wer plattformuebergreifend arbeitet, sollte diese Unterschiede kennen, bevor er sich zu stark auf JSON-Spalten als portable Loesung verlaesst.


-- MySQL: JSON-Funktionen im direkten Vergleich zu PostgreSQL-Operatoren
SELECT
  name,
  JSON_EXTRACT(attributes, '$.color') AS color_raw,
  JSON_UNQUOTE(JSON_EXTRACT(attributes, '$.color')) AS color_clean
FROM products
WHERE JSON_EXTRACT(attributes, '$.battery_hours') > 20;

-- JSON_SET aktualisiert ein einzelnes Feld, ohne das ganze Dokument
-- neu zu schreiben oder in Anwendungscode zu deserialisieren
UPDATE products
SET attributes = JSON_SET(attributes, '$.weight_grams', 305)
WHERE product_id = 1;

9. JSON-Spalten versus separate Tabelle versus NoSQL-Dokument

Die folgende Tabelle stellt die drei gaengigen Modellierungsoptionen fuer variable Attribute gegenueber und zeigt, wann JSON-Spalten die pragmatischste Wahl sind.

Kriterium Separate Tabelle (EAV) JSON-Spalte NoSQL-Dokument
Transaktionsgarantien Voll ACID Voll ACID Oft eingeschraenkt oder eventual consistent
Joins mit anderen Tabellen Nativ, aber viele Joins pro Attribut Nativ, ein Join fuer die gesamte Zeile Meist nicht oder nur eingeschraenkt moeglich
Schema-Flexibilitaet Hoch, aber komplexe Abfragen Hoch, einfache Abfragen ueber Operatoren Sehr hoch, kein Schema noetig
Operativer Aufwand Kein zusaetzliches System Kein zusaetzliches System Zusaetzliches System und Betriebsknowhow

Fuer die meisten Anwendungsfaelle mit moderat variablen Attributen sind JSON-Spalten der pragmatischste Mittelweg: die Flexibilitaet von NoSQL-Dokumenten kombiniert mit den Transaktions- und Join-Garantien der relationalen Datenbank, ohne ein zweites System betreiben zu muessen.

10. Zusammenfassung

JSON-Spalten geben relationalen Datenbanken genau die Modellierungsfreiheit, die frueher fast automatisch zu einem NoSQL-Wechsel fuehrte. jsonb in PostgreSQL und der native JSON-Typ in MySQL erlauben verschachtelte, variable Strukturen innerhalb einer einzelnen Spalte, mit vollen ACID-Garantien und normalen Joins zu anderen Tabellen. Operatoren wie ->, ->> und @> machen JSON-Inhalte direkt abfragbar, GIN-Indizes und generierte Spalten sorgen fuer Performance auch bei grossen Tabellen.

Die Faustregel bleibt entscheidend: stabile, stark referenzierte Kernfelder gehoeren als eigene Spalten in die Tabelle, variable Zusatzattribute in die JSON-Spalte. Wer diese Grenze konsequent zieht, CHECK-Constraints fuer Pflichtfelder nutzt und haeufig abgefragte Felder bei Bedarf in generierte Spalten extrahiert, bekommt mit JSON-Spalten die Flexibilitaet semi-strukturierter Daten, ohne die Garantien der relationalen Welt aufzugeben.

JSON-Spalten in relationalen Datenbanken, das Wichtigste auf einen Blick

JSONB statt JSON

In PostgreSQL fast immer JSONB verwenden, binaer gespeichert, schneller abfragbar und indizierbar.

Wann JSON-Spalten passen

Fuer variable, selten gefilterte Zusatzattribute, nicht fuer Kernfelder mit Fremdschluesseln oder Aggregaten.

Indizierung

GIN-Index fuer allgemeine Containment-Abfragen, Expression-Index fuer haeufig gefilterte Einzelfelder.

Validierung

CHECK-Constraints mit jsonb_typeof und dem Existenz-Operator sichern Pflichtfelder ab, ohne Flexibilitaet zu verlieren.

11. FAQ: JSON-Spalten in relationalen Datenbanken nutzen

1Immer JSONB statt JSON verwenden?
Fast immer ja, JSONB ist binaer gespeichert und deutlich schneller bei Abfragen und Indizierung als der Text-Typ json.
2Wann JSON-Spalte statt eigener Tabelle?
Bei stark variierenden, selten gefilterten Attributen ohne Fremdschluessel-Bedarf. Kernfelder bleiben eigene Spalten.
3Wie indiziert man JSON-Spalten?
GIN-Index fuer allgemeine Containment-Abfragen, Expression-Index fuer haeufig gefilterte Einzelfelder.
4Pflichtfelder in JSON erzwingen?
Ja, ueber CHECK-Constraints mit dem Existenz-Operator ? und jsonb_typeof fuer Typpruefungen.
5Unterschied zwischen -> und ->>?
-> liefert JSON-Wert, ->> liefert Text. Fuer Stringvergleiche und Typumwandlungen wird meist ->> gebraucht.
6Verlieren JSON-Spalten ACID-Garantien?
Nein, JSON-Spalten sind normale Spalten und nehmen an denselben Transaktionsgarantien teil wie jede andere Spalte.
7Wie ein Feld in eigene Spalte extrahieren?
Ueber eine generierte Spalte, GENERATED ALWAYS AS in PostgreSQL beziehungsweise STORED in MySQL, automatisch synchron.
8Bietet SQL Server denselben Funktionsumfang?
Nein, SQL Server speichert JSON als Text ohne binaeren Typ und bietet weniger Operatoren als PostgreSQLs jsonb.
9Ersetzt eine JSON-Spalte einen NoSQL-Wechsel?
Fuer viele Faelle ja, mit voller Transaktionssicherheit und ohne ein zweites System betreiben zu muessen.
10Wie migriert man zu einer JSON-Spalte?
Neue Spalte anlegen, Werte per UPDATE uebertragen, Code schrittweise umstellen, alte Spalten erst danach entfernen.