English version

← Learning Hub Lektion 2 Dauer: ca. 15 Min. Thema: Tabellendesign

Asset-Hierarchie & Zeitreihen in Delta

Diese Lektion zeigt dir, wie du deine Domäne — Parks, Turbinen, Wetter-Zeitreihen, Strom-Zeitreihen — als Delta-Tabellen modellierst. Am Ende kannst du Tabellenschemata skizzieren, Partitionierungsstrategien auswählen und erklären, wie dieselben Tabellen sowohl deine Produktions-Pipeline als auch deine Analysten bedienen.

Das Modellierungsproblem

Du hast drei miteinander verwobene Anforderungen, die in Delta-Tabellen koexistieren müssen:

  1. Asset-Hierarchie — ein Windpark enthält Turbinen. Du fragst auf beiden Ebenen ab.
  2. Zeitreihen — jede Messung (Wetter, Telemetrie, Prognosen) ist zeitgestempelt und wiederkehrend.
  3. Dualer Zugriff — Produktions-Pipelines lesen „die neuesten Daten für Asset X", während Analysten „alle Daten für Asset X der letzten 90 Tage" scannen.

Das Tabellenlayout richtig hinzubekommen ist deine wirkungsvollste Architekturentscheidung überhaupt. Ein schlechtes Schema bedeutet, dass du Pipelines später neu schreiben musst. Ein gutes bedeutet, dass dein Team die Plattform kaum bemerkt.

Muster 1: Die Asset-Dimensionstabelle

Trenne was Dinge sind von was mit ihnen passiert ist. Das Asset-Register ist eine langsam veränderliche Dimension — es beschreibt Parks und Turbinen, wächst aber nicht mit jedem Zeitstempel.[1]

-- Silver-Schicht: dim_assets
CREATE TABLE silver.dim_assets (
  asset_id        STRING NOT NULL,     -- z. B. "wp-nordsee-1"
  asset_type      STRING NOT NULL,     -- "wind_park" | "turbine"
  parent_asset_id STRING,              -- NULL bei Parks, Park-ID bei Turbinen
  name            STRING,
  latitude        DOUBLE NOT NULL,
  longitude       DOUBLE NOT NULL,
  hub_height_m    DOUBLE,              -- nur für Turbinen
  capacity_kw     DOUBLE,
  commissioned    DATE,
  metadata        MAP<STRING, STRING>  -- flexible Attribute
)
USING DELTA
TBLPROPERTIES ('delta.columnMapping.mode' = 'name');
Designentscheidung: flache Tabelle mit Parent-Zeiger Parks und Turbinen liegen in einer Tabelle. Die Spalte parent_asset_id erzeugt die Hierarchie. Das ist einfacher als getrennte Park-/Turbinen-Tabellen und erlaubt dir, beide Ebenen mit einem einzigen Join abzufragen. Die Geo-Position einer Turbine ist ihre eigene (nicht die des Parks) — wichtig für die präzise Wetterzuordnung.

Warum keine verschachtelten Struct-Typen?

Du könntest Turbinen als ARRAY<STRUCT> innerhalb einer Park-Zeile modellieren. Tu es nicht. Es macht Zeitreihen-Joins schmerzhaft, bricht Abfragen je Turbine und verkompliziert die geo-räumliche Wetterzuordnung. Halte es flach und joine, wenn du die Hierarchie brauchst.

Muster 2: Zeitreihen-Faktentabellen

Zeitreihen sind deine Faktentabellen — sie wachsen mit jedem Messzyklus. Das Designziel: schnelle Schreibvorgänge aus der Pipeline, schnelle Lesevorgänge für „gib mir den letzten Wert" ebenso wie für „gib mir einen Zeitraum".

Wetter-Zeitreihen (Assets zugeordnet)

-- Silver-Schicht: Wetter-Zeitreihen, auf Asset-Standorte abgebildet
CREATE TABLE silver.weather_ts (
  asset_id          STRING NOT NULL,
  timestamp         TIMESTAMP NOT NULL,
  forecast_ref_time TIMESTAMP,          -- wann die Prognose erstellt wurde
  wind_speed_ms     DOUBLE,
  wind_direction_deg DOUBLE,
  temperature_c     DOUBLE,
  irradiance_wm2    DOUBLE,
  pressure_hpa      DOUBLE,
  humidity_pct      DOUBLE,
  source_model      STRING              -- "ecmwf_ifs", "gfs", "icon"
)
USING DELTA
PARTITIONED BY (asset_id)
TBLPROPERTIES (
  'delta.autoOptimize.optimizeWrite' = 'true',
  'delta.autoOptimize.autoCompact'   = 'true'
);

Strom- / Telemetrie-Zeitreihen

-- Silver-Schicht: Stromerzeugung und Telemetrie je Turbine
CREATE TABLE silver.power_ts (
  asset_id        STRING NOT NULL,      -- auf Turbinenebene
  timestamp       TIMESTAMP NOT NULL,
  power_kw        DOUBLE,
  rotor_speed_rpm DOUBLE,
  nacelle_temp_c  DOUBLE,
  availability    DOUBLE,               -- 0,0–1,0
  status_code     STRING
)
USING DELTA
PARTITIONED BY (asset_id)
TBLPROPERTIES (
  'delta.autoOptimize.optimizeWrite' = 'true',
  'delta.autoOptimize.autoCompact'   = 'true'
);

Partitionierungsstrategie — der kritische Zielkonflikt

Die Partitionierung bestimmt, welche Dateien Spark beim Lesen überspringen kann. Machst du sie falsch, scannt jede Abfrage alles. Die Frage bei Zeitreihen lautet: nach Asset, nach Datum oder nach beidem partitionieren?

StrategieAbfragemuster PipelineAbfragemuster AnalyseFazit
PARTITIONED BY (asset_id) ✅ Schnell: „neuestes Wetter für Asset X" ⚠️ Scannt die gesamte Zeit dieses Assets Gut, wenn Assets moderate Datenmengen haben
PARTITIONED BY (date) ⚠️ Scannt alle Assets des heutigen Tages ✅ Schnell: „letzte 7 Tage über alle Assets" Gut für flottenweite Auswertungen
PARTITIONED BY (asset_id, date) ✅ In beide Richtungen schnell ✅ In beide Richtungen schnell ⚠️ Risiko: zu viele winzige Partitionen
PARTITIONED BY (asset_id) + Z-ORDER BY timestamp ✅ Schnell: Asset + Zeitraum ✅ Schnell: Asset + Zeitraum ✅ Beste Balance für die meisten Fälle
Empfehlung: nach asset_id partitionieren, Z-ORDER nach timestamp

Für eine Flotte von ca. 10–500 Windparks (mit je 5–100 Turbinen) liefert die Partitionierung nach asset_id eine handhabbare Anzahl Partitionen. Das Z-Ordering nach timestamp legt aktuelle Daten innerhalb jeder Partition zusammen — so werden sowohl „neuester Wert für die Pipeline" als auch „Zeitraum für die Analyse" schnell, ohne Tausende winziger Partitionen zu erzeugen.

Was ist Z-ORDER?

Z-Ordering ist die Data-Skipping-Optimierung von Delta Lake. Sie ordnet Daten innerhalb von Dateien physisch nach den angegebenen Spalten um, sodass Abfragen, die auf diese Spalten filtern, ganze Dateien überspringen können.[2]

-- Regelmäßig ausführen (oder per Auto-Optimize), damit Daten beieinander liegen
OPTIMIZE silver.weather_ts ZORDER BY (timestamp);
OPTIMIZE silver.power_ts   ZORDER BY (timestamp);

Nach dem Z-Ordering überspringt eine Abfrage wie WHERE asset_id = 'turbine-47' AND timestamp > '2026-07-10' die Partitionen anderer Assets (Partition Pruning) und zusätzlich Dateien mit älteren Zeitstempeln innerhalb dieser Partition (Data Skipping). Beide Zugriffspfade — Pipeline und Analyse — profitieren.

Dualen Zugriff ermöglichen

Dieselben Silver-Tabellen bedienen zwei Zielgruppen. So greift jede darauf zu:

Silver-Zeitreihen weather_ts / power_ts Delta Lake (MVCC) Einzige Quelle der Wahrheit Produktions-Pipeline Workflows / Job-Cluster „Neueste Werte für Asset X" Analysten SQL Warehouse / Notebooks „90 Tage, alle Parks" Pipeline schreibt Neue Zeitreihen anhängen Leser blockieren nie Schreiber. Schreiber blockieren nie Leser. Analysten fixieren per Time Travel eine Version — reproduzierbar.

Praktische Konsequenzen für dein Team

AnliegenWie Delta damit umgeht
Pipeline schreibt, während Analyst liest MVCC: Jeder Leser sieht einen konsistenten Snapshot. Keine Sperren.
Analyst braucht reproduzierbare Ergebnisse SELECT * FROM silver.weather_ts VERSION AS OF 42 — auf eine bestimmte Version festlegen.
Pipeline braucht „nur neue Daten seit dem letzten Lauf" Change Data Feed (CDF) oder Watermark-basierte Lesevorgänge auf timestamp.
Analyst aggregiert Parkebene aus Turbinen power_ts mit dim_assets über parent_asset_id joinen, GROUP BY Park.
Analyst entdeckt veraltete Daten DESCRIBE HISTORY silver.weather_ts — zeigt letzten Schreibzeitpunkt und Version.

Der geo-räumliche Join: Wettergitter → Asset

Wettermodelle liefern Daten auf einem Gitter (z. B. ECMWF mit 0,25° ≈ 28 km Auflösung). Deine Assets sind Punktstandorte. Die Transformation in der Silver-Schicht muss Gitterzellen auf nahegelegene Assets abbilden.

-- Vereinfacht: nächstgelegener Gitterpunkt je Asset
-- In der Praxis für höhere Genauigkeit bilineare Interpolation verwenden
SELECT
  a.asset_id,
  w.timestamp,
  w.wind_speed_ms,
  w.temperature_c,
  w.irradiance_wm2
FROM bronze.weather_raw w
JOIN silver.dim_assets a
  ON ABS(w.grid_lat - a.latitude)  < 0.125   -- halbe Gitterzelle
  AND ABS(w.grid_lon - a.longitude) < 0.125
WHERE a.asset_type IN ('wind_park', 'turbine')
Architekturentscheidung: Wo wird zugeordnet?

Der geo-räumliche Join gehört in die Transformation Bronze → Silver. Ab diesem Punkt sind alle Wetterdaten über asset_id geschlüsselt, nicht mehr über Gitterkoordinaten. Damit müssen Silver und Gold nie etwas über Gittergeometrie wissen — das vereinfacht die gesamte nachgelagerte Logik.

Wetter auf Turbinen- vs. Parkebene

Bei einem kompakten Windpark (Turbinen innerhalb von ca. 2 km) fallen womöglich alle Turbinen in dieselbe Gitterzelle — sie teilen sich also die Wetterdaten. Bei großen Offshore-Clustern, die mehrere Gitterzellen überspannen, erhalten unterschiedliche Turbinen unterschiedliches Wetter. Dein Schema deckt beides ab: Wetter wird je asset_id zugeordnet, und der Join bestimmt die Granularität.

Alles zusammen: die Schema-Karte

Silver-Schicht — Tabellenbeziehungen dim_assets PK asset_id asset_type FK parent_asset_id latitude, longitude capacity_kw (langsam veränderl. Dim.) weather_ts FK asset_id timestamp forecast_ref_time wind_speed_ms, temp_c… source_model PARTITION BY asset_id power_ts FK asset_id (Turbine) timestamp power_kw rotor_speed, nacelle_temp availability, status PARTITION BY asset_id 1:N 1:N gold.forecast_ts FK asset_id | timestamp | power_kw | confidence_low | confidence_high Zeilen auf Turbinen- und Park-Ebene ML-Modell

Anti-Muster, die du vermeiden solltest

Anti-MusterWarum es schadetStattdessen
Eine breite Tabelle mit allen Variablen Schemaänderungen (neuer Sensor) erfordern vollständiges Neuschreiben; dünn besetzte Spalten verschwenden Speicher Getrennte Faktentabellen je Domäne (Wetter, Strom, Telemetrie)
Partitionierung nach (asset_id, date) bei 500 Turbinen 500 × 365 = 182.500 Partitionen pro Jahr. Das Auflisten der Dateien wird zum Flaschenhals Nur nach asset_id partitionieren, Z-ORDER nach timestamp
Silver-Tabellen für Analysten duplizieren Datendrift zwischen den Kopien, doppelte Speicherkosten, Governance-Albtraum Eine Tabelle + MVCC. Analysten per Unity Catalog Leserechte geben
Turbinen-Arrays in Park-Zeilen einbetten Zeitreihen lassen sich ohne Explode/Unnest nicht mit einzelnen Turbinen joinen Flaches dim_assets mit Parent-Zeiger

Prüfe dein Verständnis

Warum eine flache Tabelle mit einer parent_asset_id-Spalte statt Turbinen als Arrays in Park-Zeilen zu verschachteln?

Richtig. Zeitreihentabellen sind über asset_id geschlüsselt. Wären Turbinen in Park-Zeilen verschachtelt, müsstest du vor jedem Join ein Explode/Unnest ausführen — teuer, umständlich und fehleranfällig. Eine flache Tabelle mit Parent-Zeiger gibt dir direkte FK-Joins.
Der eigentliche Grund ist die Joinbarkeit. Zeitreihen (weather_ts, power_ts) referenzieren asset_id direkt. Verschachtelte Structs würden bei jedem Join ein Explode/Unnest erzwingen — ein flaches Design mit Parent-Zeiger hält Abfragen einfach und schnell.

Welche Partitionierungsstrategie bedient bei 200 Turbinen mit 15-Minuten-Daten Pipeline und Analysten am besten?

Genau. Bei 200 Turbinen ergibt die Partitionierung nach asset_id 200 handhabbare Partitionen. Das Z-Ordering nach timestamp innerhalb jeder Partition ermöglicht effiziente Zeitraumabfragen. Die doppelte Partitionierung (asset_id, date) würde 200 × 365 = 73.000 Partitionen pro Jahr erzeugen — viel zu feingranular.
Nach asset_id partitionieren + Z-ORDER nach timestamp ist der Sweet Spot. Du bekommst Partition Pruning nach Asset UND Data Skipping nach Zeitraum, ohne das Small-File-Problem der doppelten Partitionierung.

Wo in der Medaillon-Architektur gehört der geo-räumliche Join (Wettergitter → Asset-Standort) hin?

Genau. Bronze speichert die rohen Gitterdaten unverändert. Die Transformation Bronze → Silver löst Gitterkoordinaten zu asset_ids auf. Ab Silver ist alles über Asset geschlüsselt — das vereinfacht die gesamte nachgelagerte Logik (Feature Engineering, ML, Gold-Aggregation).
Der geo-räumliche Join gehört in den Schritt Bronze → Silver. Bronze bleibt roh (Gitterkoordinaten intakt). Silver führt Asset-geschlüsselte Zeitreihen ein. So müssen sich nachgelagerte Schichten (Gold, Feature Store, ML) nie mit Gittergeometrie befassen.

Wie aggregiert ein Analyst Strom auf Turbinenebene zur Parkebene, ohne eine separate Park-Tabelle?

Richtig. Die parent_asset_id in dim_assets verknüpft jede Turbine mit ihrem Park. Joine power_ts → dim_assets, dann GROUP BY parent_asset_id mit SUM(power_kw) — und du hast die Summen auf Parkebene. Das ist die Stärke des flachen Hierarchiemodells.
Die Lösung nutzt den Parent-Zeiger. Joine power_ts mit dim_assets (um parent_asset_id zu bekommen) und gruppiere dann per GROUP BY parent_asset_id. So aggregierst du Turbinendaten über die in dim_assets hinterlegte Hierarchie auf Parkebene.

Primärquelle

📖 Empfohlene Lektüre

Delta Lake best practices — Databricks-Dokumentation. Behandelt Partitionierung, Z-Ordering, Auto-Optimize und Tabellendesign. Konzentriere dich auf die Abschnitte „Choose the right partition column" und „Compact files with optimize" — sie unterfüttern die Entscheidungen dieser Lektion direkt.

Wie geht es weiter

Du weißt jetzt, wie du Assets und Zeitreihen in Delta modellierst. Die nächste Lektion behandelt Ingestion-Pipelines — wie Wetterdaten tatsächlich ankommen (GRIB2-Dateien, API-Abrufe), geparst werden und in Bronze landen. Wir schauen uns Zeitpläne, inkrementelle Ladevorgänge und das Werkzeug (cfgrib, xarray) an, das Wetterdaten für Spark zugänglich macht.


Fragen? Willst du tiefer in Partitionierung, Z-Ordering oder den geo-räumlichen Join einsteigen? Frag deinen Lehrer — ich kann dir detailliertere Beispiele zeigen oder Abwägungen für deine konkrete Flottengröße besprechen.

Rechtliches: Impressum