Kapitoly kurzu
1. Obsahová mapa kurzu 2. Notebooky: PySpark vs Pure Python 3. Demo: Tři cesty a Mini Medallion 4. CU Costs a Capacity Metrics 5. SQL trojúhelník 6. Real-Time Intelligence: demo arc 7. Semantic modely, DAX a optimalizace 8. Security a administrace Fabricu 9. Zkouška DP-600: tipy
Runbooky — medallion
10. Mini Medallion ČHMÚ 11. Mini Medallion NYC Taxi 12. NYC Taxi třemi cestami 13. Mini Medallion OpenAQ 14. Big Medallion OpenAQ (2 roky)
Runbooky — RTI
15. RTI Library: architektura 16. A: Bicycles 60s 17. B: ČHMÚ HTTP polling 18. C: NDIC dopravní detektory 19. D: Camera ALPR simulátor 20. E: PID GTFS-RT 21. F: Activator alerty 22. KQL cheat sheet
🏠 Rozcestník kurzu
DP-600 · Runbook

Runbook: Mini Medallion ČHMÚ

Ovzduší ČHMÚ → Lakehouse → Warehouse → Power BI, krok za krokem, s ověřenými fixy
📓 Runbook · medallion / batch

Cíle kapitoly

  • Nahrát lookup CSV a snapshot ČHMÚ do Lakehouse a uložit vše jako Delta tabulky
  • Číst Lakehouse tabulky z Warehouse přes 3-part naming
  • Postavit Bronze, Silver dimenze, Silver fakt a Gold přes T-SQL procedury
  • Vědět, které cesty (COPY INTO z OneLake, Load to table, deltalake lib) selhaly a proč
10.1

Úvod

Flat playbook s reálnými fixy z prvního pokusu. Všechny systematické chyby vyřešené.

Klíčové ponaučení:

  • COPY INTO z OneLake do Warehouse selhává na Access token couldn't be fetched → NE-používáme
  • Lakehouse "Load to table" UI selhává na CHMI hlavičce s leading whitespace → NE-používáme
  • deltalake Python knihovna má os.rename bug na Lakehouse mountu → PySpark kernel místo Python kernel, používáme spark.write.format("delta").saveAsTable()

Strategie: PySpark notebook nahraje všechna data jako Delta tabulky do Lakehouse Tables/. Warehouse je pak konzumuje přes 3-part-naming <LakehouseName>.dbo.<table>. Cross-database query je v Fabricu auto-povolená.

10.2

Setup (jednorázově)

  1. Workspace v Fabricu (např. DP600) na F2 / trial kapacitě
  2. Lakehouse v workspace (např. název DP600)
  3. Warehouse v workspace (např. název DP600_DW — musí být odlišný od Lakehouse jména)
ℹ️
Poznámka:

Pozn.: Tento runbook používá DP600 jako jméno Lakehouse. Pokud máš jiné, nahraď ve všech 3-part-naming SQL referencích (DP600.dbo.xxx).

💡
Doporučení pro live demo: dej Lakehouse a Warehouse VÝRAZNĚ odlišná jména (např. LH_Ovzdusi a WH_Ovzdusi), abys při demu omylem nezaklikl špatný item. DP600 jako jméno obojího je matoucí.
10.3

Upload lookup CSVs do Lakehouse Files (UI)

V Lakehouse DP600:

  1. Levý panel → Files → ⋯ → New subfolder → název lookups
  2. Otevři lookups, klikni Upload → Upload files
  3. Vyber 3 soubory: dim_kraj.csv, dim_pollutant.csv, dim_station.csv (stáhněte ze sekce Lookup soubory níže)
  1. Klikni Upload, počkej na ✓

Stejně tak vytvoř Files/raw/ (zatím prázdné, použijeme za chvíli).

✓
Verify: Files → lookups → 3 CSV soubory.
10.4

Notebook (PySpark kernel): stáhnout CHMI + uložit všechno jako Delta

⚠️
Pozor: Notebook MUSÍ mít kernel PySpark (Python). Cell 1 vypadá jako čistý Python (urllib, open) a v PySpark kernelu funguje stejně. Cell 2 ale potřebuje spark builtin, který je k dispozici JEN v PySpark kernelu, ne v Python kernelu.
  1. Workspace → + New → Notebook
  2. DŮLEŽITÉ — Language dropdown vlevo nahoře: musí být "PySpark (Python)", NE "Python". "Python" kernel nemá spark builtin a Cell 2 selže s name 'spark' is not defined.
  3. VLEVO V PANELU: Add Lakehouse → Existing → vyber DP600 → klikni "+ Make this Lakehouse default"
    • Tohle je kritický krok — bez něj selže write přes Files/...

Cell 1 — Stáhnout CHMI snapshot do Lakehouse Files

PYTHON
import urllib.request, os

url = "https://opendata.chmi.cz/air_quality/now/data/airquality_1h_avg_CZ.csv"
raw = urllib.request.urlopen(url).read()
os.makedirs("/lakehouse/default/Files/raw", exist_ok=True)
with open("/lakehouse/default/Files/raw/chmi_snapshot.csv", "wb") as f:
    f.write(raw)
print(f"OK, {len(raw)} bajtů zapsáno do Files/raw/chmi_snapshot.csv")

Run → očekáváno: OK, ~18000 bajtů. První cell v PySpark notebooku trvá 60-120 s kvůli Spark cold start (i když Cell 1 sama Spark nepoužívá). Druhá Cell už proběhne rychle.

Cell 2 — Nahrát všechny CSVs jako Delta tabulky do Tables/

PYTHON
csv_to_table = [
    ("Files/raw/chmi_snapshot.csv",     "chmi_snapshot_raw"),
    ("Files/lookups/dim_kraj.csv",      "dim_kraj_raw"),
    ("Files/lookups/dim_pollutant.csv", "dim_pollutant_raw"),
    ("Files/lookups/dim_station.csv",   "dim_station_raw"),
]

for path, table in csv_to_table:
    df = (spark.read
          .option("header", "true")
          .option("ignoreLeadingWhiteSpace", "true")  # CHMI má ", " za čárkou
          .option("inferSchema", "true")
          .csv(path))
    df.write.format("delta").mode("overwrite").saveAsTable(table)
    print(f"  {table}: {df.count()} rows")

Očekáváno:

TEXT
chmi_snapshot_raw: ~470 rows
dim_kraj_raw:        14 rows
dim_pollutant_raw:   10 rows
dim_station_raw:    201 rows
✓
Verify: v Lakehouse Explorer pravý panel → ⋯ vedle Tables → Refresh → vidíš všechny 4 tabulky.
10.5

Wait + refresh SQL endpoint

Lakehouse SQL endpoint má sync delay. Než to budeš zkoušet z Warehouse:

  1. Workspace → najdi item type "SQL analytics endpoint" se jménem DP600 (vedle Lakehouse a Warehouse)
  2. Otevři ho
  3. V levém Explorer panelu → pravý klik na schema dbo → Refresh
  4. Sanity check: spusť SELECT TOP 5 * FROM dbo.chmi_snapshot_raw; — měla by vrátit data

Pokud table není vidět, počkej 60-120 s a refresh znovu. Po prvním sync to bývá rychlé.

10.6

Bronze layer (Warehouse SQL editor)

Otevři Warehouse DP600_DW → New SQL query.

Cell 4.1 — Schema + tabulka

SQL
CREATE SCHEMA bronze;
GO

CREATE TABLE bronze.chmi_measurement (
    idRegistration INT,
    startTime VARCHAR(30),
    idValueType INT,
    value DECIMAL(10,3),
    _source_url VARCHAR(200),
    _ingested_at DATETIME2(6)
);

Cell 4.2 — Load přes 3-part-naming z Lakehouse

SQL
INSERT INTO bronze.chmi_measurement (idRegistration, startTime, idValueType, value)
SELECT idRegistration, startTime, idValueType, value
FROM DP600.dbo.chmi_snapshot_raw;

UPDATE bronze.chmi_measurement
SET _ingested_at = SYSDATETIME(),
    _source_url = 'https://opendata.chmi.cz/air_quality/now/data/airquality_1h_avg_CZ.csv'
WHERE _ingested_at IS NULL;

SELECT COUNT(*) AS bronze_rows FROM bronze.chmi_measurement;

Očekáváno: bronze_rows = ~470.

10.7

Silver dimensions

Cell 5.1 — Schema + tabulky

SQL
CREATE SCHEMA silver;
GO

CREATE TABLE silver.dim_kraj (
    kraj_id SMALLINT NOT NULL,
    kraj_kod VARCHAR(3),
    kraj_nazev VARCHAR(50),
    nuts_kod VARCHAR(5)
);

CREATE TABLE silver.dim_pollutant (
    id_value_type SMALLINT NOT NULL,
    kod VARCHAR(10),
    nazev_cs VARCHAR(50),
    nazev_en VARCHAR(50),
    jednotka VARCHAR(10),
    limit_eu_year_avg DECIMAL(6,2),
    limit_eu_24h DECIMAL(6,2),
    note VARCHAR(200)
);

CREATE TABLE silver.dim_station (
    id_registration INT NULL,
    loc_code VARCHAR(4) NOT NULL,
    nazev VARCHAR(80),
    mesto VARCHAR(50),
    kraj_id SMALLINT,
    lat DECIMAL(7,4),
    lon DECIMAL(7,4),
    nadm_vyska_m INT NULL,
    typ_stanice VARCHAR(20),
    loc_owner VARCHAR(20),
    classification VARCHAR(20),
    id_confidence VARCHAR(20)
);

Cell 5.2 — Load přes 3-part-naming

SQL
INSERT INTO silver.dim_kraj
SELECT CAST(kraj_id AS SMALLINT), kraj_kod, kraj_nazev, nuts_kod
FROM DP600.dbo.dim_kraj_raw;

INSERT INTO silver.dim_pollutant
SELECT CAST(id_value_type AS SMALLINT), kod, nazev_cs, nazev_en, jednotka,
       TRY_CAST(limit_eu_year_avg AS DECIMAL(6,2)),
       TRY_CAST(limit_eu_24h AS DECIMAL(6,2)),
       note
FROM DP600.dbo.dim_pollutant_raw;

INSERT INTO silver.dim_station
SELECT TRY_CAST(id_registration AS INT), loc_code, nazev, mesto,
       CAST(kraj_id AS SMALLINT),
       TRY_CAST(lat AS DECIMAL(7,4)),
       TRY_CAST(lon AS DECIMAL(7,4)),
       TRY_CAST(nadm_vyska_m AS INT),
       typ_stanice, loc_owner, classification, id_confidence
FROM DP600.dbo.dim_station_raw;

SELECT 'kraj' AS tbl, COUNT(*) AS rows FROM silver.dim_kraj
UNION ALL SELECT 'pollutant', COUNT(*) FROM silver.dim_pollutant
UNION ALL SELECT 'station', COUNT(*) FROM silver.dim_station;

Očekáváno: kraj=14, pollutant=10, station=201.

10.8

Silver fact

Cell 6.1 — Tabulka

SQL
CREATE TABLE silver.fact_measurement (
    id_registration INT,
    loc_code VARCHAR(4) NULL,
    mereni_ts DATETIME2(6),
    datum DATE,
    hodina SMALLINT,
    id_value_type SMALLINT,
    value DECIMAL(10,3),
    kraj_id SMALLINT,
    kraj_nazev VARCHAR(50),
    mesto VARCHAR(50),
    station_nazev VARCHAR(80),
    typ_stanice VARCHAR(20),
    pollutant_kod VARCHAR(10),
    pollutant_nazev VARCHAR(50),
    jednotka VARCHAR(10),
    je_nad_limitem BIT,
    _silver_loaded_at DATETIME2(6)
);

Cell 6.2 — Procedure + run

SQL
CREATE OR ALTER PROCEDURE silver.sp_load_fact_measurement
AS
BEGIN
    TRUNCATE TABLE silver.fact_measurement;

    INSERT INTO silver.fact_measurement
    (id_registration, loc_code, mereni_ts, datum, hodina,
     id_value_type, value,
     kraj_id, kraj_nazev, mesto, station_nazev, typ_stanice,
     pollutant_kod, pollutant_nazev, jednotka, je_nad_limitem,
     _silver_loaded_at)
    SELECT
        b.idRegistration,
        s.loc_code,
        TRY_CONVERT(DATETIME2(6), REPLACE(b.startTime, 'Z', '')),
        TRY_CONVERT(DATE, REPLACE(b.startTime, 'Z', '')),
        DATEPART(HOUR, TRY_CONVERT(DATETIME2(6), REPLACE(b.startTime, 'Z', ''))),
        b.idValueType,
        b.value,
        ISNULL(s.kraj_id, -1),
        ISNULL(k.kraj_nazev, 'Unmapped'),
        ISNULL(s.mesto, 'Unknown'),
        ISNULL(s.nazev, 'ID ' + CAST(b.idRegistration AS VARCHAR(20))),
        ISNULL(s.typ_stanice, 'unknown'),
        p.kod,
        p.nazev_cs,
        p.jednotka,
        CASE
            WHEN p.limit_eu_year_avg IS NOT NULL AND b.value > p.limit_eu_year_avg THEN 1
            WHEN p.limit_eu_24h IS NOT NULL AND b.value > p.limit_eu_24h THEN 1
            ELSE 0
        END,
        SYSDATETIME()
    FROM bronze.chmi_measurement b
    LEFT JOIN silver.dim_station s ON s.id_registration = b.idRegistration
    LEFT JOIN silver.dim_kraj k ON k.kraj_id = s.kraj_id
    LEFT JOIN silver.dim_pollutant p ON p.id_value_type = b.idValueType
    WHERE b.value IS NOT NULL;

    SELECT COUNT(*) AS silver_rows,
           SUM(CASE WHEN id_registration IS NULL OR kraj_id = -1 THEN 1 ELSE 0 END) AS unmapped
    FROM silver.fact_measurement;
END;
GO

EXEC silver.sp_load_fact_measurement;

Očekáváno: silver_rows ≈ 470, unmapped ≈ 469 (pouze stanice 40237=LLIL Liberec-Rochlice je bridged).

10.9

Gold

Cell 7.1 — Tabulka

SQL
CREATE SCHEMA gold;
GO

CREATE TABLE gold.fact_kvalita_ovzdusi (
    datum DATE,
    hodina SMALLINT,
    kraj_id SMALLINT,
    kraj_nazev VARCHAR(50),
    pollutant_kod VARCHAR(10),
    pollutant_nazev VARCHAR(50),
    jednotka VARCHAR(10),
    pocet_stanic INT,
    prum_value DECIMAL(10,3),
    max_value DECIMAL(10,3),
    min_value DECIMAL(10,3),
    pocet_nad_limitem INT,
    _gold_loaded_at DATETIME2(6)
);

Cell 7.2 — Procedure + run

SQL
CREATE OR ALTER PROCEDURE gold.sp_load_kvalita_ovzdusi
AS
BEGIN
    TRUNCATE TABLE gold.fact_kvalita_ovzdusi;

    INSERT INTO gold.fact_kvalita_ovzdusi
    SELECT
        datum, hodina,
        kraj_id, kraj_nazev,
        pollutant_kod, pollutant_nazev, jednotka,
        COUNT(DISTINCT id_registration),
        AVG(value), MAX(value), MIN(value),
        SUM(CAST(je_nad_limitem AS INT)),
        SYSDATETIME()
    FROM silver.fact_measurement
    WHERE kraj_id <> -1
      AND pollutant_kod IS NOT NULL
    GROUP BY datum, hodina, kraj_id, kraj_nazev,
             pollutant_kod, pollutant_nazev, jednotka;

    SELECT COUNT(*) AS gold_rows FROM gold.fact_kvalita_ovzdusi;
END;
GO

EXEC gold.sp_load_kvalita_ovzdusi;

SELECT TOP 50 * FROM gold.fact_kvalita_ovzdusi;

Očekáváno: gold_rows malé (1-5) — protože jen 1 stanice (LLIL Liberec) je bridged.

10.10

Power BI report (volitelně)

Power BI Desktop → Get Data → OneLake catalog → vyber DP600_DW → Direct Lake → vyber gold.fact_kvalita_ovzdusi (+ silver.dim_* pro slicery).

Vizualizace stejné jako v kapitole 3 (closure dema): KPI karty, mapa krajů, top 10 stanic, trend a compliance ribbon. S jedinou bridged stanicí bude report chudý — smysl dává až po doplnění bridge tabulky idRegistration ↔ loc_code.

10.11

Lookup soubory

Tři lookup CSV použité v KROKU 1 (veřejná data ČHMÚ a číselníky, CC BY 4.0):