Nested Data Modeling im Delta Lake: JSON, STRUCT oder Variant?

Zwei Kollegen vor einem Whiteboard mit der Überschrift „Nested Modeling: pro Feldgruppe entscheiden“: eine event.json-Payload verzweigt je Feldgruppe: stabile Felder wie adresse werden zu STRUCT<straße, plz, stadt>, BI-Kennzahlen wie betrag/währung werden flach, heterogene Rohdaten landen als raw: Variant, dazu der Hinweis _rescued_data überwachen
Lesezeit8 Min
Zuletzt aktualisiert15.7.2026
Zusammenfassung

Die Kernaussagen auf einen Blick.

  • Nested Modeling im Delta-Lakehouse bedeutet: Verschachtelte Daten werden mit STRUCT, ARRAY, MAP oder Variant gespeichert, statt JSON in jedem Query neu zu parsen.
  • Die Entscheidung fällt pro Attributgruppe: stabile Felder bleiben als STRUCT, BI-Kennzahlen werden flach modelliert und heterogene Payloads können als Variant gespeichert werden.
  • Typische Fehler sind zu tiefe Verschachtelung, fehlende bei wechselnden Schemas und Auto-Loader-Jobs ohne klare Schema-Hints.
  • Mein Vorschlag: Die Modellierungsentscheidung schon beim Ingest treffen. Schema-Hints setzen, überwachen, Variant nur für wirklich variable Payloads nutzen und möglichst in den Gold-Layer verschieben.
01

Warum semi-strukturierte Daten schon beim Ingest modelliert werden müssen

App-Events, IoT-Payloads und Cloud-Logs haben oft dasselbe Problem: Sie liefern verschachteltes JSON mit vielen Feldern, von denen viele selten gefüllt sind, und das Schema ändert sich regelmäßig. Wer diese Daten ins Lakehouse lädt und erst im Query entscheidet, wie sie gelesen werden, zahlt doppelt. Einmal durch wiederholtes JSON-Parsing in Abfragen. Und ein zweites Mal, weil nachgelagerte Tabellen und Reports bei Schemaänderungen instabil werden.

Star Schema, Wide Tables und Data Vault ordnen wir in der Datenmodellierung im Lakehouse für den Delta-Gold-Layer ein. Semi-strukturierte Quellen kommen dort nicht vor. Für semi-strukturierte Daten braucht es eine eigene Modellierungsentscheidung. Welche Felder bleiben verschachtelt? Welche werden flach modelliert? Und welche gehören in eine Variant-Spalte?

Ab wann ist ein EXPLODE in jeder Abfrage teurer als eine flache Tabelle, die schon beim Ingest vorbereitet wird? Wie verhindert _rescued_data, dass neue Felder den Stream brechen, ohne dass die Spalte unbemerkt voll läuft? Wann trägt eine Variant-Spalte für CloudTrail besser als eine typisierte STRUCT-Definition? Warum werden BI- und Semantik-Layer schwer nutzbar, sobald der Silver-Layer zu tief verschachtelt ist?

02

Was ist Nested Data Modeling im Delta-Lakehouse?

Nested Modeling nutzt vier Delta-native Typen für verschachtelte Daten (Databricks Docs zu Model semi-structured data):

STRUCT

Bündelt fest definierte Unterfelder wie user.id, user.email und user.country. Erlaubt Column Pruning bis auf Sub-Feld-Ebene.

ARRAY

Speichert eine geordnete Folge gleichartiger Elemente, zum Beispiel mehrere Produkte in einem Warenkorb.

MAP

Hält offene Schlüssel-Wert-Paare mit einheitlichem Werte-Typ, etwa frei definierte Tags oder Traits.

Variant

Typoffener JSON-Container ab Databricks Runtime 15.3, speichert ohne Vorab-Schema und bleibt trotzdem pro Sub-Pfad abfragbar.

Parquet legt STRUCT-Sub-Felder und ARRAY-Elemente spaltenweise ab (Dremel-Encoding mit Repetition- und Definition-Levels), sodass eine Abfrage nicht die ganze Struktur scannen muss. Variant wird Teil von Spark 4.0 und Delta 4.0 und liegt im Parquet-Format als zusätzliche Gruppe mit metadata- und value-Binär-Spalte vor, wie im Databricks-Blog zum Variant-Typ beschrieben. Variant kann deutlich schneller sein als JSON als STRING, weil der ganze JSON-Text nicht bei jeder Abfrage neu geparst werden muss.

Eine orders-Tabelle könnte so aussehen: order_id als BIGINT, kunde als STRUCT mit name, email und verschachtelter adresse (strasse, plz, stadt), positionen als ARRAY<STRUCT> mit sku, menge und preis, varianten als MAP<STRING,STRING> sowie ein audit-STRUCT mit source und loaded_at. Der Query-Pfad läuft dann in Dot-Notation: SELECT kunde.adresse.plz, positionen[0].sku FROM orders.

Delta unterstützt semi-strukturierte Quellen wie JSON, Avro oder Protobuf ausdrücklich und rät explizit von rein normalisierten 3NF-Modellen im Analytics-Layer ab. Zur Wahl stehen STRUCT, ARRAY, MAP, Variant oder im Ausnahmefall JSON-STRING.

Wie sich diese semi-strukturierte Perspektive gegen die Wide-Table-Denormalisierung im Gold-Layer positioniert, zeigen wir im Überblick zu One Big Table im Delta-Lakehouse.

Data & AI Beratung mit ruhrdot

Als offizieller Databricks-Partner begleiten wir dich von der Datenstrategie bis zur produktiven KI-Lösung.

Databricks Logo
03

welche Modellierung passt wann?

Die Modellierungsentscheidung fällt pro Feldgruppe. Eine Event-Tabelle kann Kontextdaten als STRUCT behalten und wichtige Felder wie event_type, revenue oder product_id zusätzlich flach speichern. Der Fehler wäre, alle JSON-Felder nach derselben Regel zu behandeln.

FeldgruppeSignalModellierungQuery-Muster
Stabile Unterfelder, die später gezielt abgefragt werden5–20 gut besetzte Felder, seltene Schema-ÄnderungenSTRUCT beibehalten, im Silver-Layer nestedmit Column Pruning auf Sub-Feld
Wiederholte Elemente, die später aggregiert werdenLine-Items, Tags, Attribut-Listen mit hoher Zeilen-MultiplikationARRAY im Silver, flach im Gold-Aggregatenur im Gold-Layer, nicht in Ad-hoc-BI
Offene Schlüssel mit einheitlichem Werte-Typ, benutzerdefinierte Tags, dynamische AttributeMAP im Silvermit Type-Cast ohne Column-Pruning
Stark wechselnde JSON-Payloads mit unterschiedlichen FeldstrukturenJSON aus 3–5 Firmware-Versionen mit unterschiedlichen FeldbäumenVariant ab DBR 15.3 als typoffener ContainerVariant-Path-Navigation mit Dot-Notation, keine Schema-Vordefinition
BI-relevante Metriken für klassisches SQL-AggregatTop-Level-Kennzahlen für Dashboards mit klarem Fach-BezugFlach denormalisieren in den Gold-LayerStandard-SQL ohne Nested-Zugriff
Selten befragte, schemalose Zusatzfelder als Rescue-FangbeckenFirmware-neue Felder ohne KonsumentenSTRING mit Monitoringmanuelle Auswertung, keine produktiven Queries

Diese Entscheidung wirkt sich direkt auf Bronze, Silver und Gold aus. Bronze hält das Roh-JSON und erzwingt nur das nötigste Schema. Silver flacht Arrays für Aggregate, materialisiert Kernfelder aus STRUCTs und behält die Sub-Strukturen dort, wo Downstream-Konsumenten sie brauchen. Gold liefert das aggregierte, semantisch klare Endprodukt ohne JSON-Blobs und ohne tief verschachtelte Strukturen. Die Regel dahinter: alles, was BI klassisch aggregiert und dessen Konsumenten die Struktur ignorieren, gehört flach in den Gold-Layer. Alles mit Downstream-Iteration über Sub-Records, hoher Schema-Change-Rate oder klar getrennter Semantik bleibt nested. Praktisch wird es ab mehr als drei Ebenen schwierig. Viele BI-Tools und Semantik-Layer werden dann unübersichtlich oder schwer nutzbar.

04

Typische Quellen für semi-strukturierte Daten im Lakehouse

Drei Quelltypen kommen besonders häufig vor. Jeder bringt ein eigenes Modellierungsmuster mit.

App-Event-Streams

Segment, Snowplow: context bleibt als STRUCT, properties wird für Kernfelder wie revenue und product_id materialisiert, traits als MAP.

IoT-Payloads

MQTT, AWS IoT, Azure IoT Hub: stabiler Grundrahmen aus device, readings, metadata (zwei Ebenen), variabler Sensor-Teil als Variant oder STRUCT mit _rescued_data-Fallback.

Log-Streams

CloudTrail, Web-Access-Logs: meist stabile, dokumentierte Struktur, typisierte STRUCT-Definition, einzelne Top-Level-Felder für Fach-Analysen im Silver-Layer.

Databricks nennt ähnliche Einsatzfelder für Variant: Endpoint Detection and Response mit variablen Log-JSON-Schemas, Ad-Click-Analyse mit unbekannten Payload-Feldern und IoT-Telemetrie. Das deckt sich mit dem Muster oben.

05

Was kosten STRUCT, ARRAY, MAP und JSON-STRING im Query?

STRUCT-Zugriff ist meist günstig. Parquet legt jedes Sub-Feld eines STRUCT als eigene Spalte ab; das Dremel-Encoding kodiert Verschachtelungs-Tiefe über Repetition und Definition Levels (Apache-Parquet-Spec zu Nested Encoding). Wenn nur context.user.id gebraucht wird, kann die Engine gezielt dieses Unterfeld lesen, statt die gesamte Struktur zu verarbeiten. Column Pruning und Data Skipping funktionieren auf Nested-Feldern.

ARRAY-Explode kann teuer werden. EXPLODE vervielfacht die Zeilenanzahl abhängig von der Länge des Arrays und braucht vor Aggregation meist eine GROUP BY-Zusammenführung. Kombiniert mit einem WHERE-Filter auf ein STRUCT-Feld innerhalb des Arrays wird der Query-Plan schnell mehrstufig. Die praktische Regel: EXPLODE möglichst im Gold-Layer vorbereiten und nicht jedes Mal in interaktiven BI-Abfragen ausführen.

MAP ist flexibel, hat aber Nachteile: weniger Column Pruning, weniger Statistiken und oft zusätzliche Casts beim Zugriff auf einzelne Keys. Für dichte MAPs mit 20 stabilen Keys ist ein STRUCT die bessere Wahl.

JSON als STRING ist meist die teuerste Option, sobald Felder regelmäßig abgefragt werden. Jede Abfrage, die ein Sub-Feld braucht, parst den kompletten JSON-Blob neu, entweder mit from_json und expliziter Schema-Angabe oder mit :-Path-Notation. Bei Query-Frequenz jenseits von Ad-hoc wird der CPU-Overhead sichtbar. Variant schließt diese Lücke: flexibler als STRUCT, aber deutlich besser abfragbar als ein reiner JSON-STRING.

06

Wie funktioniert JSON-Ingestion mit Auto Loader?

Auto Loader ist ein typischer Baustein, um JSON-Dateien nach Delta zu laden. Für die Modellierung ist wichtig, wo die erwarteten Felder und Typen festgelegt werden: Schema-Hints und die _rescued_data-Spalte. Wie Auto Loader technisch funktioniert, führen wir aus im Überblick zum Auto Loader auf Databricks.

Bei JSON und CSV erkennt Auto Loader Felder standardmäßig oft sehr vorsichtig, zum Beispiel als STRING. Deshalb sollte die gewünschte Struktur explizit angegeben werden. cloudFiles.schemaHints erzwingt Ziel-Typen für bekannte Felder, nested-Typen und Arrays inklusive:

Damit wird klar festgelegt, welche Felder welchen Typ haben sollen. Alles andere bleibt der Inferenz überlassen.

Die _rescued_data-Spalte sammelt Felder und Werte, die nicht ins erwartete Schema passen: fehlende Felder in der Ziel-Definition, Type-Mismatches, Case-Mismatches. Der Payload landet als JSON-Blob mit Herkunfts-Pfad in dieser Spalte. Für Quellen mit häufigen Schemaänderungen sollte _rescued_data eingeplant werden. Ohne die Spalte bricht der Stream, sobald ein neues Feld auftaucht. _rescued_data braucht auch Monitoring. Wenn regelmäßig viele Daten dort landen, passt das Ziel-Schema nicht mehr zur Quelle.

Für die typisierte Deserialisierung im Silver-Layer stehen mehrere Funktionen bereit. from_json mit expliziter StructType-Definition ist der stabile Weg für typisierte Sub-Felder mit Schema als Code. to_json serialisiert zurück, falls Downstream-Konsumenten wieder JSON-Text erwarten. schema_of_json hilft beim Verstehen unbekannter JSON-Strukturen, sollte aber nicht die produktive Schema-Definition ersetzen. Für Variant-Spalten übernimmt parse_json die Konvertierung aus einem JSON-STRING, und Sub-Feld-Zugriffe laufen über den :-Path-Operator statt über Dot-Notation. Für seltene Ad-hoc-Zugriffe auf archivierte JSON-Strings kann get_json_object reichen.

Häufiger Denkfehler
Die _rescued_data-Spalte wird beim Auto-Loader-Setup weggelassen, weil das erwartete Schema zum Startzeitpunkt vollständig wirkt. Sobald eine neue Firmware- oder App-Version ein unbekanntes Feld liefert, bricht der Stream beim ersten nicht passenden Datensatz.
Was hilft
_rescued_data von Anfang an in jedem Auto-Loader-Job einplanen und die Rest-Rate aktiv überwachen. Steigt sie deutlich an, passt das Ziel-Schema nicht mehr zur Quelle. Das ist der Trigger für eine geplante Schema-Anpassung, nicht für einen Produktions-Incident.
07

Wie lässt sich Schema-Evolution bei Nested Schemas steuern?

Nested Schemas ändern sich, und Delta liefert vier Modi für den Umgang mit neuen Feldern via cloudFiles.schemaEvolutionMode (Databricks Docs zu Auto Loader Schema-Evolution). addNewColumns erweitert das Schema, pausiert den Stream und erzwingt einen kontrollierten Restart. So können Downstream-Tabellen bewusst angepasst werden. addNewColumnsWithTypeWidening erweitert das Schema und kann bestimmte Typen verbreitern (Integer zu Long); Unpassendes landet in _rescued_data. rescue hält das Schema stabil und schreibt neue oder unpassende Felder in _rescued_data. failOnNewColumns bricht bei neuen Feldern ab und passt für Umgebungen mit strengem Change Management.

Column Mapping (delta.columnMapping.mode = 'name') kann Umbenennungen in verschachtelten Feldern ermöglichen, ohne die Dateien neu zu schreiben. Ein context.user.email kann zu context.user.email_address werden, ohne dass die Parquet-Dateien neu geschrieben werden. Die physische Speicherung bleibt stabil, während die logischen Feldnamen geändert werden können. Wie Column Mapping mit Time Travel und Change Data Feed zusammenspielt, klären wir im Überblick zum Change Data Feed.

Für den Betrieb ist entscheidend: addNewColumns als Default-Modus mit Alert-Mechanismus auf die pausierten Streams. Schema-Hints als festgelegtes Zielschema für die bewusst geplanten Felder. _rescued_data mit Monitoring auf die Rest-Rate. So bleibt das Modell anpassbar, ohne Schemaänderungen unkontrolliert durchlaufen zu lassen.

08

Wann lohnt sich Variant statt STRUCT oder JSON-STRING?

Variant lohnt sich vor allem in drei Fällen. Erstens: JSON-Payloads ändern sich häufig, aber einzelne Unterfelder werden trotzdem regelmäßig abgefragt. Zweitens: heterogene Logs mit variablen Attribut-Bäumen. Drittens: Bestehende JSON-STRING-Spalten werden oft abgefragt und verursachen spürbaren Parse-Aufwand.

Es gibt aber klare Grenzen. Variant setzt Databricks Runtime 15.3 oder höher voraus (Public Preview zum Zeitpunkt der Veröffentlichung; die Roadmap führt Variant in Spark 4.0 und Delta 4.0). STRUCT und MAP müssen für Variant über to_variant_object() konvertiert werden; MAP-Schlüssel müssen dabei STRING sein.

Vor der Variant-Einführung
Iceberg v2-Tabellen unterstützen kein Variant. Die Iceberg-Kompatibilität kommt erst mit Iceberg v3. Wer gleichzeitig Delta und Iceberg unterstützen muss, sollte diese Einschränkung vor der Einführung prüfen.

Der Databricks-Benchmark zeigt achtfache Lese-Performance gegenüber String-Storage bei nested- und flat-Schemas auf Runtime 15.0 mit Photon. Das ist ein Databricks-Benchmark und sollte im eigenen Workload geprüft werden. Die Größenordnung ist plausibel, weil Variant Sub-Feld-Zugriffe ohne Ganz-Blob-Parsing erlaubt.

Der Mechanismus dahinter: Häufig genutzte Pfade können intern spaltenorientiert abgelegt werden. Häufig genutzte Sub-Pfade werden als versteckte columnar Sub-Spalten materialisiert, während der Rest als Binär-Blob im value-Feld bleibt. Column Pruning greift dadurch auf Sub-Pfad-Ebene, ähnlich wie bei STRUCTs, ohne dass ein Vorab-Schema definiert werden muss. Für Nutzer bleibt das weitgehend transparent. Entscheidend ist, dass häufig genutzte Pfade schneller gelesen werden können.

Die Migration aus JSON-STRING läuft über PARSE_JSON(json_string_column), aus MAP über to_variant_object(map_column). Für neue Ingest-Strecken lohnt Variant, wenn die Payloads stark wechseln und trotzdem regelmäßig einzelne Felder abgefragt werden. Wenn das Schema stabil ist, bleibt STRUCT meist einfacher und sauberer.

09

Grenzen von Nested Modeling im Delta-Lakehouse

Nested Modeling ist hilfreich, erzeugt aber typische Probleme, wenn nicht klar entschieden wird, welche Felder nested bleiben und welche flach werden.

Zu tiefe Verschachtelung

BI-Tools wie Power BI und Tableau haben Grenzen beim Umgang mit tief verschachtelten Delta-Tabellen. Praktisch sollten es höchstens drei Ebenen sein.

Wechselnde Payloads ohne _rescued_data

Ohne _rescued_data bricht der Stream beim ersten unbekannten Feld einer neuen Firmware. Bei unkontrollierter Schemaentwicklung muss die Spalte eingeplant und überwacht werden.

JSON als STRING ohne Materialisierung

from_json in jedem Query zahlt bei hoher Query-Frequenz teuer. Ohne Variant gehört wenigstens eine Materialisierung der Kernfelder in den Silver-Layer.

EXPLODE als Standardlösung

Ein EXPLODE in der interaktiven BI-Query multipliziert Zeilen bei jedem Zugriff. Aggregate gehören in den Gold-Layer, nicht in die interaktive Session.

Variant-Overuse

Variant löst nicht jede Schemafrage, sondern verschiebt einen Teil der Strukturentscheidung in die Abfrage. Für stabile, dichte Sub-Felder bleibt STRUCT typsicherer.

MAP für eigentlich stabile Felder

Wer 20 fest definierte Keys als MAP speichert, verliert Column Pruning und Statistik-Sammlung. Für dichte Keys ist STRUCT die bessere Wahl.

Die größten Probleme sind selten rein technisch. Meist fehlt eine klare Modellierungsregel. Nested Modelle brauchen mehr Modellierungs-Disziplin als flache Tabellen, weil die Entscheidung pro Feldgruppe fällt und dokumentiert werden muss. Ein Silver-Modell ohne Schema-Hints und ohne Monitoring von _rescued_data bleibt langfristig instabil. Es verschiebt das Schema-Problem in die Zukunft, bis der nächste Firmware-Wechsel den Ingest bricht.

10

Fazit

Nested Modeling ist im Delta-Lakehouse die passende Antwort auf viele semi-strukturierte Quellen. Die Entscheidung sollte pro Feldgruppe getroffen werden.

STRUCT passt für stabile Unterfelder, die gezielt abgefragt werden. ARRAY passt für wiederholte Elemente, solange EXPLODE nicht in jeder BI-Abfrage passiert. MAP passt für offene Schlüssel, wenn die Werte einen einheitlichen Typ haben. Variant passt für heterogene Payloads, wenn JSON als STRING zu teuer wird und STRUCT zu starr ist. Schema-Hints und _rescued_data machen diese Entscheidung im Ingest nachvollziehbar und kontrollierbar.

Nächster Schritt

Entscheidet beim Ingest, nicht erst im ersten produktiven Query: Auto Loader mit Schema-Hints als festgelegtes Zielschema, `_rescued_data` mit Monitoring, Column Mapping für bewegliche Umbenennungen, Variant nur ab Runtime 15.3 und nur für die Feldgruppen, deren Schema-Wechsel real ist. Und die Regel für die Nesting-Tiefe: maximal drei Ebenen, sonst wird der Silver-Layer für BI-Tools und Semantik-Layer schwer nutzbar.

Alexander Rabe
Alexander Rabe
Co-Founder · Head of Data & AI
11

FAQ

Nested Data Modeling nutzt STRUCT, ARRAY, MAP und Variant, um verschachtelte JSON-Daten im Delta-Lakehouse strukturiert zu speichern. So müssen Daten nicht immer komplett flach gemacht oder als reiner JSON-STRING abgelegt werden. Parquet als Speicherformat unterstützt Nested-Encoding über Repetition- und Definition-Levels, sodass Column Pruning bis auf Sub-Feld-Ebene funktioniert. Die Entscheidung fällt pro Feldgruppe und sollte schon beim Ingest festgelegt werden.