This lesson teaches you how to model your domain — parks, turbines, weather time series, power time series — as Delta tables. By the end, you'll be able to sketch table schemas, choose partitioning strategies, and explain how the same tables serve both your production pipeline and your analysts.
You have three interrelated concerns that must coexist in Delta tables:
Getting the table layout right is your single highest-leverage architectural decision. A bad schema means rewriting pipelines later. A good one means your team barely notices the platform.
Separate what things are from what happened to them. The asset registry is a slowly-changing dimension — it describes parks and turbines but doesn't grow with every timestamp.[1]
-- Silver layer: dim_assets
CREATE TABLE silver.dim_assets (
asset_id STRING NOT NULL, -- e.g. "wp-nordsee-1"
asset_type STRING NOT NULL, -- "wind_park" | "turbine"
parent_asset_id STRING, -- NULL for parks, park ID for turbines
name STRING,
latitude DOUBLE NOT NULL,
longitude DOUBLE NOT NULL,
hub_height_m DOUBLE, -- turbine-specific
capacity_kw DOUBLE,
commissioned DATE,
metadata MAP<STRING, STRING> -- flexible attrs
)
USING DELTA
TBLPROPERTIES ('delta.columnMapping.mode' = 'name');
parent_asset_id column creates the hierarchy. This is simpler than separate park/turbine tables and lets you query both levels with a single join. A turbine's geo-location is its own (not the park's) — important for precise weather mapping.
You could model turbines as an ARRAY<STRUCT> inside a park row. Don't. It makes time series joins painful, breaks per-turbine queries, and complicates the geo-spatial weather mapping. Keep it flat, join when you need hierarchy.
Time series are your fact tables — they grow with every measurement cycle. The design goal: fast writes from the pipeline, fast reads for both "give me the latest" and "give me a range."
-- Silver layer: weather time series mapped to asset locations
CREATE TABLE silver.weather_ts (
asset_id STRING NOT NULL,
timestamp TIMESTAMP NOT NULL,
forecast_ref_time TIMESTAMP, -- when the forecast was issued
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'
);
-- Silver layer: power output and telemetry per turbine
CREATE TABLE silver.power_ts (
asset_id STRING NOT NULL, -- turbine-level
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'
);
Partitioning determines which files Spark can skip when reading. Get it wrong and every query scans everything. The question for time series is: partition by asset, by date, or both?
| Strategy | Pipeline Query Pattern | Analyst Query Pattern | Verdict |
|---|---|---|---|
PARTITIONED BY (asset_id) |
✅ Fast: "latest weather for asset X" | ⚠️ Scans all time for that asset | Good if assets have moderate data volumes |
PARTITIONED BY (date) |
⚠️ Scans all assets for today | ✅ Fast: "last 7 days across all assets" | Good for fleet-wide analytics |
PARTITIONED BY (asset_id, date) |
✅ Fast both ways | ✅ Fast both ways | ⚠️ Risk: too many small partitions |
PARTITIONED BY (asset_id) + Z-ORDER BY timestamp |
✅ Fast: asset + time range | ✅ Fast: asset + time range | ✅ Best balance for most cases |
For a fleet of ~10–500 wind parks (each with 5–100 turbines), partitioning by asset_id gives you manageable partition counts. Z-ordering by timestamp then co-locates recent data within each partition — making both "latest for pipeline" and "range for analysis" fast without creating thousands of tiny partitions.
Z-ordering is Delta Lake's data-skipping optimisation. It physically reorders data within files by the specified column(s), so queries that filter on those columns can skip entire files.[2]
-- Run periodically (or via auto-optimize) to keep data co-located
OPTIMIZE silver.weather_ts ZORDER BY (timestamp);
OPTIMIZE silver.power_ts ZORDER BY (timestamp);
After Z-ordering, a query like WHERE asset_id = 'turbine-47' AND timestamp > '2026-07-10' skips the partition for other assets (partition pruning) and skips files with older timestamps within that partition (data skipping). Both access paths — pipeline and analyst — benefit.
The same Silver tables serve two audiences. Here's how each accesses them:
| Concern | How Delta Handles It |
|---|---|
| Pipeline writes while analyst reads | MVCC: each reader sees a consistent snapshot. No locks. |
| Analyst needs reproducible results | SELECT * FROM silver.weather_ts VERSION AS OF 42 — pin to a specific version. |
| Pipeline needs "only new data since last run" | Change Data Feed (CDF) or watermark-based reads on timestamp. |
| Analyst aggregates park-level from turbines | Join power_ts to dim_assets on parent_asset_id, GROUP BY park. |
| Stale data detected by analyst | DESCRIBE HISTORY silver.weather_ts — see last write timestamp and version. |
Weather models produce data on a grid (e.g., ECMWF at 0.25° ≈ 28 km resolution). Your assets are point locations. The Silver-layer transformation must map grid cells to nearby assets.
-- Simplified: nearest grid point to each asset
-- In practice, use bilinear interpolation for higher accuracy
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 -- half grid cell
AND ABS(w.grid_lon - a.longitude) < 0.125
WHERE a.asset_type IN ('wind_park', 'turbine')
The geo-spatial join lives in the Bronze → Silver transformation. After this point, all weather data is keyed by asset_id, not grid coordinates. This means Silver and Gold never need to know about grid geometry — simplifying all downstream logic.
For a compact wind park (turbines within ~2 km), all turbines may map to the same grid cell — so they share weather data. For large offshore clusters spanning multiple grid cells, different turbines get different weather. Your schema handles both: weather is mapped per asset_id, and the join determines granularity.
| Anti-Pattern | Why It Hurts | Instead |
|---|---|---|
| One wide table with all variables | Schema changes (new sensor) require full rewrite; sparse columns waste storage | Separate fact tables per domain (weather, power, telemetry) |
Partitioning by (asset_id, date) with 500 turbines |
500 × 365 = 182,500 partitions/year. File listing becomes the bottleneck | Partition by asset_id only, Z-ORDER by timestamp |
| Duplicating Silver tables for analysts | Data drift between copies, double storage cost, governance nightmare | Single table + MVCC. Grant analysts read-only via Unity Catalog |
| Embedding turbine arrays in park rows | Can't join time series to individual turbines without explode/unnest | Flat dim_assets with parent pointer |
Why use a flat table with a parent_asset_id column instead of nesting turbines as arrays inside park rows?
For 200 turbines with 15-minute interval data, what partitioning strategy best serves both pipeline and analysts?
Where in the medallion architecture does the geo-spatial join (weather grid → asset location) belong?
How does an analyst aggregate turbine-level power to park-level without a separate park table?
📖 Recommended Reading
Delta Lake best practices — Databricks Documentation. Covers partitioning, Z-ordering, auto-optimize, and table design. Focus on the "Choose the right partition column" and "Compact files with optimize" sections — they directly inform the decisions in this lesson.
You now know how to model assets and time series in Delta. The next lesson will cover ingestion pipelines — how weather data actually arrives (GRIB2 files, API pulls), gets parsed, and lands in Bronze. We'll look at scheduling, incremental loads, and the tooling (cfgrib, xarray) that makes weather data accessible to Spark.
Questions? Want to dive deeper into partitioning, Z-ordering, or the geo-spatial join? Ask your teacher — I can show you more detailed examples or discuss trade-offs specific to your fleet size.
Rechtliches: Impressum