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 · Kapitola 3

Demo: Tři cesty a Mini Medallion

Scénář hlavního dema nad daty ČHMÚ: Bronze třemi cestami, medallion v T-SQL, closure v Power BI
🗂 Kapitola kurzu

Cíle kapitoly

  • Odvyprávět story arc dema nad daty ČHMÚ od CSV URL k Power BI reportu
  • Načíst stejný snapshot třemi cestami: Notebook, Dataflow Gen2 a Pipeline
  • Postavit mini medallion Bronze → Silver → Gold přes T-SQL procedury ve Warehouse
  • Připravit pre-buildy a fallbacky, aby demo přežilo výpadek Sparku nebo endpointu
  • Vyhnout se anti-patternům, které demo živě položí
3.1

Úvod

Lektorský scénář pro hlavní demo Day 1+2. Postaveno nad CHMI air quality daty (kvalita ovzduší v ČR, aktuální i historická).

ℹ️
Proč CHMI a ne nehody: Nehody PČR distribuuje data jako RAR + XLS (dvě překážky před prvním ingestem). CHMI publikuje přímý CSV URL bez auth — perfektní pro demo. Přepoužitelné na další kurzy a konferenční dema.

Story arc dema:

  1. Story setup — co je Lakehouse, end-to-end flow, ukázka cílového PBI reportu
  2. Bronze, 3 cesty — stejný CHMI snapshot načten třemi způsoby (Notebook / DF Gen2 / Pipeline) do stejné Lakehouse Files zóny
  3. Mini Medallion přes Warehouse + SQL procedury — Bronze → Silver → Gold přes T-SQL
  4. Closure — Power BI report s mapou krajů + trendy polutantů

Demo se v Day 1 rozprostírá mezi lekce (ne najednou):

  • Story setup → před lekcí 1.1 End-to-end overview
  • Cesta 1 (Notebook) → během lekce 1.5 Spark
  • Cesta 2 (DF Gen2) → během lekce 1.3 Dataflow Gen2
  • Cesta 3 (Pipeline) → během lekce 1.4 Pipelines
  • Mini Medallion → běží na pozadí, fragmenty napříč Day 2
  • Closure PBI → konec Day 1, jako "to je kam míříme v Day 3"
3.2

Tenant strategie — kdo kde sedí

Rozdělení:

  • Demo tenant = lektorův, F2 kapacita, start/stop mode (pauza mimo demo bloky)
  • Účastníci = vlastní tenant, lektor do něj nepřistupuje
  • Lektor demuje, účastníci sledují — žádné share screen na účastníkův tenant
  • F64 trial workspace v lektorově tenantu jako bonus srovnání rychlosti, pokud je čas

Důsledky pro demo:

  1. Před každým demo blokem resume kapacitu F2 (10–20 s lag než je ready)
  2. Po dni demo pause kapacitu → CU billing stop
  3. Anti-pattern: pokud potřebuješ ukázat publish flow, použij screenshot, ne live
3.3

Pre-kurz příprava

A. Lookup tabulky

Tři lookup CSV (dim_kraj, dim_pollutant, dim_station) jsou hotové ke stažení v runbooku Mini Medallion ČHMÚ. dim_station.csv je odvozený z oficiálního registru stanic ČHMÚ (GeoPackage na geoportal.gov.cz, CC BY 4.0) — ~200 stanic s GPS, krajem a typem.

⚠️
Pozor: ČHMÚ používá dva ID systémy: live API má numerické idRegistration, registr písmenné loc_code. Bridge mezi nimi v open datech není, takže většina měření skončí po joinu jako Unmapped. Je to autentický real-world scénář a demo s tím počítá (viz Silver procedura níže).

Hotový dim_station.csv jde přepoužít pro každý další kurz — registr se mění zřídka.

B. Workspace setup

TEXT
Tenant:    <tenant lektora>
Workspace: DP600_Demo_CHMI (F2 kapacita, start/stop)
Lakehouse: ovzdusi_demo
Warehouse: ovzdusi_dw
Optional:  DP600_Demo_F64_Compare (na F64 trial pro speed-up srovnání)

C. Files struktura v Lakehouse

SCHÉMA
Files/
├── lookups/
│   ├── dim_kraj.csv
│   ├── dim_pollutant.csv
│   └── dim_station.csv (po validaci)
├── raw/
│   └── (Bronze ingest cíl — plní se v demu)
└── recent/
    └── (volitelně pre-stáhnutý 30denní snapshot pro fallback)

Upload lookup CSVs jednou před kurzem přes Lakehouse Explorer → Upload.

D. Pre-buildy pro fallback

Aktivum Jak Účel
01_Bronze_Notebook Hotový PySpark notebook Když live padne, otevřeš tenhle
Bronze_Dataflow Hotový Dataflow Gen2 Backup pro DF cestu
Bronze_Pipeline Hotová pipeline Backup
silver.fact_measurement Pre-vytvořená Delta tabulka Pokud Bronze run selže
gold.fact_kvalita_ovzdusi Pre-vytvořená Pokud cokoliv padne
ovzdusi_demo.pbix Hotový PBI report Pro closure dema

E. Pre-run dopředu pro Capacity Metrics

Den před kurzem (24 h zpět = data v Metrics app už budou):

  1. Spustit všechny 3 Bronze cesty + Silver + Gold v F2 workspace
  2. Ráno před kurzem screenshot z Capacity Metrics
3.4

Story setup (před lekcí 1.1)

Talk track:

"Co budeme dělat. Dataset = aktuální kvalita ovzduší v ČR od ČHMÚ. Hodinové průměry pro všechny stanice (~470 stanic), ~10 polutantů: PM10, NO2, ozon, SO2, benzen, CO. Cílem je dostat se z živého CSV URL k Power BI dashboardu, který odpoví na otázky typu 'Který kraj má nejhorší ovzduší dnes' nebo 'Kdy je v Brně nejvíc smogu'.

Cestou ukážu tři různé způsoby, jak nahrát stejná data: notebook, Dataflow, Pipeline. Pokaždé se dostaneme na stejné místo. Pak postavíme mini medallion — Bronze (raw), Silver (čištěno), Gold (agregováno) — přes SQL procedury do Warehouse. Na závěr Day 1 ukážu výsledný report.

Klíčový výhoda CHMI dat: jeden URL, žádná autentizace, žádná extrakce, čistý CSV. Tenhle dataset si po kurzu můžete vzít a přepoužít na vlastní firemní data — workflow je úplně stejný.

Demo poběží na F2 kapacitě — to je nejmenší placená kapacita Fabricu, asi 250 USD/měsíc. Uvidíte, jak se to chová. Spark se na F2 startuje 1–2 minuty — během toho budu povídat o architektuře."

ℹ️
Ukázat hned na začátku: finální PBI report (statický screenshot nebo PBIX otevřený offline). Účastníci pak ví, "kam jdeme".
3.5

CHMI dataset — schema cheat

URL: https://opendata.chmi.cz/air_quality/now/data/airquality_1h_avg_CZ.csv

Charakter: snapshot poslední hodinové průměry, ~470 řádků (1 řádek = 1 stanice × 1 polutant), refresh každou hodinu.

Schema (4 sloupce, comma-delimited, UTF-8, leading whitespace):

Sloupec Typ Význam
idRegistration int CHMI kód stanice (5-digit, např. 10221, 40237)
startTime timestamp UTC Začátek hodinového intervalu (ISO 8601 s Z)
idValueType int Kód polutantu (8=NO2, 5=PM10, 113=PM2.5, 6=O3, 1=SO2, ...)
value float Naměřená hodnota (jednotka per polutant: µg/m³ nebo mg/m³)
⚠️
KRITICKÉ při čtení: skipinitialspace=True — CHMI má za čárkou mezeru.
PYTHON
df = pd.read_csv(url, skipinitialspace=True)  # ne sep=";", ne encoding=

Pro multi-hour history použij recent/ adresář s rolling oknem:

TEXT
https://opendata.chmi.cz/air_quality/recent/data/airquality_1h_avg_CZ_YYYYMMDDHH.csv

(Naming convention nutno ověřit při validation runu.)

3.6

Cesta 1: Notebook (PySpark)

ℹ️
Kdy v Day 1: během lekce 1.5 Apache Spark.

Talk track úvod:

"První cesta — programmatic. Notebook, PySpark. Tohle volíte když máte data engineera, vyhovuje vám kód, potřebujete logiku co Dataflow neumí, nebo chcete reusable knihovnu transformací."

Notebook: Demo_01_Bronze_PySpark

PYTHON
from pyspark.sql.functions import col, lit, current_timestamp

import urllib.request
url = "https://opendata.chmi.cz/air_quality/now/data/airquality_1h_avg_CZ.csv"
raw = urllib.request.urlopen(url).read().decode("utf-8")
print(f"Staženo {len(raw)} bajtů")
ℹ️
Pozn. pro lektora: Spark spark.read.csv() umí jen dbfs:/, abfss://, file:/// — nikoliv https:// přímo. Workaround = stáhnout přes Python, uložit do Files/, pak Spark čte z Files.
PYTHON
mssparkutils.fs.put(
    "Files/raw/chmi_snapshot.csv",
    raw,
    overwrite=True,
)

df = (spark.read
    .option("header", "true")
    .option("delimiter", ",")
    .option("inferSchema", "true")
    .option("ignoreLeadingWhiteSpace", "true")  # ekvivalent skipinitialspace
    .csv("Files/raw/chmi_snapshot.csv"))

print(f"Načteno řádků: {df.count():,}")
df.printSchema()
df.show(5)
PYTHON
df_bronze = (df
    .withColumn("_source_url", lit(url))
    .withColumn("_ingested_at", current_timestamp())
    .withColumn("_ingest_method", lit("notebook_pyspark"))
)

(df_bronze.write
    .format("delta")
    .mode("overwrite")
    .option("overwriteSchema", "true")
    .saveAsTable("ovzdusi_bronze"))

spark.sql("SELECT COUNT(*) FROM ovzdusi_bronze").show()
spark.sql("DESCRIBE DETAIL ovzdusi_bronze").show(truncate=False)

Co říct během cells:

  • Cell 2: "Spark sám HTTPS nečte. Použijeme Python na stažení, mssparkutils na zápis. Trochu šachy, ale produkční scénář."
  • Cell 3: "ignoreLeadingWhiteSpace — CHMI má za každou čárkou mezeru. Bez toho by sloupce měly bizarní názvy."
  • Cell 4: "_source_url, _ingested_at = lineage. Bronze nikdy bez lineage."

Pure Python alternativa pro F2 šetření:

PYTHON
import pandas as pd, urllib.request
df = pd.read_csv(url, skipinitialspace=True)
from deltalake import write_deltalake
write_deltalake("/lakehouse/default/Tables/ovzdusi_bronze", df, mode="overwrite")

Talk: "Pro 470 řádků je Spark zbytečný overhead. Pure Python notebook startuje instantně. Pro celý DP-600 worth knowing — ne všechno musí být PySpark."

Risky body:

  • Spark cold start (1–2 min na F2) — naplánuj povídání o Spark architecture
  • Pokud Spark zhavaruje, otevři pre-built notebook a ukaž že "tady už to běželo"
3.7

Cesta 2: Dataflow Gen2

Kdy v Day 1: během lekce 1.3 Dataflow Gen2.

Talk track úvod:

"Druhá cesta — low-code. Dataflow Gen2 = Power Query v Fabricu. Tohle volíte když máte analytika z Power BI, který už zná Power Query, a chcete mu dát ETL nástroj bez kódu. UI je shodné s Power Query Online z Power BI Service."

Click-through:

Step Akce Co říct
1 Workspace → New → Dataflow Gen2 "Začínáme prázdný"
2 Get data → Web → URL: https://opendata.chmi.cz/air_quality/now/data/airquality_1h_avg_CZ.csv "Power Query umí čtení z Web zdrojů přímo, žádný proxy"
3 File Origin: 1252: Western European (Windows) nebo 65001: UTF-8 "CHMI je UTF-8, default by měl projít"
4 Delimiter: Comma –
5 OK → preview, pojmenuj query chmi_snapshot_bronze "Tohle je PQ M language pod kapotou"
6 V transformacích: použij Trim na sloupcích jmen — kvůli leading whitespace v CHMI "Trim hlavičky uklidí mezery"
7 V Home → Data destination → Lakehouse → vyber ovzdusi_demo → New table → ovzdusi_bronze_dataflow Klíčový moment: ukaž že DF Gen2 má destinations
8 Update method: Replace –
9 Save & Run –
10 Po 1–2 min ukázat Refresh history → success –

Klíčové talk pointy:

  • "Pod kapotou je M language. Můžete přepnout do Advanced editor a vidět kód."
  • "Destination = Lakehouse, ale taky Warehouse, KQL DB nebo SQL DB. Big advantage proti Dataflow Gen1."
  • "Fast Copy mode (od 2024) — DF Gen2 detekuje, kdy může jet jako Pipeline copy."
  • "Web zdroj je největší výhoda Dataflow Gen2 pro tenhle case. Spark by tu samou věc načítal přes mezikrok do Files. DF Gen2 streamuje rovnou."

Risky body:

  • Destination configurace má někdy bug — když "Save & Run" hodí error, vyresetovat: smazat destinaci, znovu nastavit
  • Refresh history v jiném submenu — Workspace → tři tečky vedle DF → "View refresh history"
3.8

Cesta 3: Data Pipeline (Copy Activity)

Kdy v Day 1: během lekce 1.4 Pipelines.

Talk track úvod:

"Třetí cesta — orchestrace. Pipeline. Tohle volíte když máte složitější flow: stáhnout z S3, přesunout, vyčistit, zavolat notebook, poslat email. Pipeline je orchestrátor."

Click-through:

Step Akce Co říct
1 Workspace → New → Data pipeline → název chmi_bronze_pipeline –
2 Activities → Copy data assistant (wizard) "Wizard projde i začátečníka"
3 Source: HTTP → URL: https://opendata.chmi.cz/air_quality/now/data/airquality_1h_avg_CZ.csv "HTTP source = direct web copy, žádný staging mezi"
4 Anonymous auth –
5 File format: CSV → encoding UTF-8, delimiter Comma, header true –
6 Destination: Lakehouse → ovzdusi_demo → Tables → New table → ovzdusi_bronze_pipeline –
7 Settings: enable staging? No pro demo (zjednoduší) –
8 Review → Save + Run –
9 Po 30–60 s success → Output tab → ukázat metriky (řádky, KB, s) "Tohle je co Pipeline metricky vidí"

Klíčové talk pointy:

  • "Pipeline má 30+ aktivit: Copy Data, Notebook, Stored Procedure, ForEach, If, Wait, Web. Když potřebujete větvit, podmínkovat, paralelizovat — pipeline."
  • "Schedule trigger v záložce Schedule. Daily ETL by tady stál."
  • "Pro CHMI use case: Pipeline by stahovala hodinový snapshot každou hodinu. Schedule Trigger → Recurrence → 1 hour."

Risky body:

  • HTTP source někdy potřebuje wait+retry (CHMI občas timeoutuje)
  • Schedule UI bývá pomalá k loadnutí
3.9

Mini Medallion: Warehouse + SQL procedury

Kdy v Day 2: napříč lekcemi 2.1 (Warehouse start), 2.2 (Load data), 2.3 (Query).

Talk track úvod:

"Teď ukážu, jak to vypadá v praxi pro tým s SQL backgroundem. Místo Spark notebooku napíšeme T-SQL. Bronze → Silver → Gold přes stored procedures ve Warehouse. Pro tým se SQL Server backgroundem je tohle nejnižší overhead — víte všechno, jen syntax v Fabric Warehouse má pár nuancí."

Step 1: Vytvořit Warehouse + Bronze tabulku

ℹ️
Poznámka: Fabric Warehouse nepodporuje typy nvarchar, tinyint, datetime ani money — proto všude varchar (UTF-8 kolace), smallint a datetime2. Ověřený běh je v runbooku Mini Medallion ČHMÚ; COPY INTO z OneLake tam selhalo na tokenu, spolehlivá cesta je 3-part naming z Lakehouse.
SQL
-- Bronze landing: mirror CSV struktury
CREATE SCHEMA bronze;
GO

CREATE TABLE bronze.chmi_measurement (
    idRegistration INT,
    startTime VARCHAR(30),       -- ISO 8601 string, parsujeme později
    idValueType INT,
    value DECIMAL(10,3),
    _source_url VARCHAR(200),
    _ingested_at DATETIME2
);

Step 2: Load do Bronze přes COPY INTO

SQL
COPY INTO bronze.chmi_measurement (idRegistration, startTime, idValueType, value)
FROM 'https://onelake.dfs.fabric.microsoft.com/<workspace_id>/<lakehouse_id>/Files/raw/chmi_snapshot.csv'
WITH (
    FILE_TYPE = 'CSV',
    FIELDTERMINATOR = ',',
    FIRSTROW = 2,
    ENCODING = 'UTF8'
);

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(*) FROM bronze.chmi_measurement;  -- ~470
💡
Talk track: "COPY INTO je multi-tenant friendly, transactional, nepotřebuje pipeline. Pro snapshot ingest do Warehouse to nejlepší volba."

Step 3: Lookup tabulky (Silver dependencies)

Lookup CSVs už máš v Files/lookups/ (z pre-kurz přípravy). Načti je:

SQL
CREATE SCHEMA silver;
GO

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

COPY INTO silver.dim_kraj
FROM 'https://onelake.dfs.fabric.microsoft.com/<ws>/<lh>/Files/lookups/dim_kraj.csv'
WITH (FILE_TYPE = 'CSV', FIRSTROW = 2, ENCODING = 'UTF8');

-- dim_pollutant
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)
);

COPY INTO silver.dim_pollutant
FROM 'https://onelake.dfs.fabric.microsoft.com/<ws>/<lh>/Files/lookups/dim_pollutant.csv'
WITH (FILE_TYPE = 'CSV', FIRSTROW = 2, ENCODING = 'UTF8');

-- dim_station
-- POZOR: ČHMÚ má dva ID systémy.
-- Live API: idRegistration (numeric, např. 10221, 40237)
-- Registry: loc_code (alphabetic 4-letter, např. ABRE, LLIL)
-- Bridge tabulka v open data není. id_registration je NULL pro většinu řádků.

CREATE TABLE silver.dim_station (
    id_registration INT NULL,        -- bridge na live API (většinou NULL)
    loc_code VARCHAR(4) NOT NULL,     -- EOI/EEA kód (de facto PK)
    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),            -- ČHMÚ, ZÚ Ústí nL, SMBrno, atd.
    classification VARCHAR(20),        -- B/U/RN, T/U/C, atd. (typ/zóna/zdroj)
    id_confidence VARCHAR(20)         -- HIGH_BRIDGED, HIGH_REGISTRY
);

COPY INTO silver.dim_station
FROM 'https://onelake.dfs.fabric.microsoft.com/<ws>/<lh>/Files/lookups/dim_station.csv'
WITH (FILE_TYPE = 'CSV', FIRSTROW = 2, ENCODING = 'UTF8');

SELECT 'kraj' AS dim, 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
UNION ALL SELECT 'station_with_bridge', COUNT(*) FROM silver.dim_station WHERE id_registration IS NOT NULL;
💡
Talk track: "Tři lookup tabulky tvoří dimenzionální páteř. Důležitá realita: ČHMÚ live API a registr stanic používají dva různé ID systémy. dim_station má 201 stanic z oficiálního registru, ale bridges (numeric idRegistration → loc_code) jsou jen pro pár ověřených. Pro účastníky je to autentický real-world scénář — když joinujete s nedokonalými dimenzemi, většina dat skončí jako 'Unmapped'. To je téma samo o sobě."

Step 4: Stored procedure Bronze → Silver

SQL
CREATE OR ALTER PROCEDURE silver.sp_load_fact_measurement
AS
BEGIN
    IF OBJECT_ID('silver.fact_measurement', 'U') IS NOT NULL
        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, REPLACE(b.startTime, 'Z', '')) AS mereni_ts,
        TRY_CONVERT(DATE, REPLACE(b.startTime, 'Z', ''))      AS datum,
        DATEPART(HOUR, TRY_CONVERT(DATETIME2, REPLACE(b.startTime, 'Z', ''))) AS hodina,
        b.idValueType,
        b.value,
        ISNULL(s.kraj_id, -1)       AS kraj_id,
        ISNULL(k.kraj_nazev, N'Unmapped') AS kraj_nazev,
        ISNULL(s.mesto, N'Unknown') AS mesto,
        ISNULL(s.nazev, N'ID ' + CAST(b.idRegistration AS VARCHAR(20))) AS station_nazev,
        ISNULL(s.typ_stanice, 'unknown') AS typ_stanice,
        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 AS je_nad_limitem,
        SYSDATETIME()
    FROM bronze.chmi_measurement b
    LEFT JOIN silver.dim_station s ON s.id_registration = b.idRegistration  -- bridge join
    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_stations,
           SUM(je_nad_limitem) AS over_limit_count
    FROM silver.fact_measurement;
END;
GO

CREATE TABLE silver.fact_measurement (
    id_registration INT,
    loc_code VARCHAR(4) NULL,
    mereni_ts DATETIME2,
    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
);

EXEC silver.sp_load_fact_measurement;

Talk track:

  • "TRY_CONVERT místo CONVERT — invalid timestamp neshodí celý insert."
  • "LEFT JOIN s ISNULL fallback — kraj_id = -1 a station_nazev = 'ID 12345' pro unmapped stanice. To je realistický pattern: live data má víc stanic než registr, část měření je vždy 'neznámá'. Pro Gold layer pak filtrujeme WHERE kraj_id <> -1 nebo to ukážeme oddělenou kategorií."
  • "je_nad_limitem = computed flag. Star schema-friendly, BI tým nemusí psát CASE WHEN v PBI."
  • "Druhý SELECT na konci procedury hlásí počet unmapped — sledovat jako data quality metric."

Step 5: Gold — agregace pro PBI

SQL
CREATE SCHEMA gold;
GO

CREATE OR ALTER PROCEDURE gold.sp_load_kvalita_ovzdusi
AS
BEGIN
    IF OBJECT_ID('gold.fact_kvalita_ovzdusi', 'U') IS NOT NULL
        TRUNCATE TABLE gold.fact_kvalita_ovzdusi;

    -- Agregace per kraj × polutant × hodina
    INSERT INTO gold.fact_kvalita_ovzdusi
    (datum, hodina, kraj_id, kraj_nazev,
     pollutant_kod, pollutant_nazev, jednotka,
     pocet_stanic, prum_value, max_value, min_value,
     pocet_nad_limitem, _gold_loaded_at)
    SELECT
        datum, hodina,
        kraj_id, kraj_nazev,
        pollutant_kod, pollutant_nazev, jednotka,
        COUNT(DISTINCT id_registration) AS pocet_stanic,
        AVG(value)                       AS prum_value,
        MAX(value)                       AS max_value,
        MIN(value)                       AS min_value,
        SUM(CAST(je_nad_limitem AS INT)) AS pocet_nad_limitem,
        SYSDATETIME()
    FROM silver.fact_measurement
    WHERE kraj_id <> -1            -- Skip unmapped stanice v Gold (mají 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

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
);

EXEC gold.sp_load_kvalita_ovzdusi;
SELECT TOP 20 * FROM gold.fact_kvalita_ovzdusi
ORDER BY pocet_nad_limitem DESC, prum_value DESC;

Talk track: "Gold = star schema fakt. Pro snapshot ~ 14 krajů × 3-10 polutantů × 1 hodina = ~150 řádků. Pro multi-day historii násobeno hodinami × dny — pořád BI-friendly velikost."

Step 6: Volitelně — orchestrace přes pipeline

TEXT
Pipeline activities:
  1. Stored Procedure: silver.sp_load_fact_measurement
  2. Stored Procedure: gold.sp_load_kvalita_ovzdusi
  3. Send Teams notification "Done"

Talk: "Tohle je production setup. Schedule denně 03:00 (po ranní ingestní pipeline), on-failure email."

3.10

Closure: Power BI report

ℹ️
Kdy: konec Day 1 (15:50 ish), nebo začátek Day 3.

Pre-built .pbix obsah

Source: DirectLake na gold.fact_kvalita_ovzdusi. Tím se ukáže Direct Lake feature v Day 3.

5 vizualizací:

  1. KPI cards (top of canvas):

    • Aktuální průměr PM10 v ČR (µg/m³)
    • Stanic nad limitem (count)
    • Nejhorší kraj právě teď
  2. Mapa krajů (filled map nebo Azure Maps):

    • Color = průměrná hodnota PM10 (nebo vybraný polutant)
    • Tooltip: počet stanic, max value, % nad limitem
    • Slicer pro polutant
  3. Stacked bar — Top 10 nejhorších stanic:

    • Y axis: stanice
    • Bars: hodnota polutantu
    • Color: typ stanice (urban / traffic / industrial / background)
  4. Line chart — denní trend (pokud máš multi-hour data):

    • X axis: hodina
    • Y axis: hodnota
    • Multi-line: per polutant
  5. Compliance ribbon:

    • Bar per kraj
    • Color: zelená/žlutá/červená dle % stanic nad limitem

Slicery: Polutant (PM10/NO2/O3...), Kraj, Datum/hodina.

Talk track:

"Tohle je výsledek. ~470 řádků raw → ~150 řádků agregovaných → tenhle dashboard. Direct Lake mode = report čte přímo z Delta Parquet, žádný import refresh. Když Pipeline každou hodinu doplní data, dashboard je aktuální za sekundy.

Tohle je úplný end-to-end flow Fabricu. Stejný workflow byste použili pro vlastní firemní data — zaměnit URL CHMI za vlastní zdroj, dim_station za vaše entity, a dashboard se sebe-aktualizuje."

3.11

Anti-cheat list (co NEDĚLAT live)

  • Nezkoušej spark.read.csv() přímo HTTPS — Spark to neumí, je třeba mezikrok přes Files
  • Nepoužívej inferSchema=true v Spark — projde data 2× a v Day 1 chce každá vteřina šetřit
  • Nezačínej demo Capacity Metrics live. Latence 5–30 min na F2 = uvidíš svůj run zítra. Pre-screenshot.
  • Nepřepínej kapacitu během runu. Když je workspace na F2, zůstaň. F64 srovnání = jiný workspace, ne migrace.
  • Lektor demuje, účastníci sledují — žádné share screen na tenant účastníků
  • NESDÍLEJ cesty k souborům s ID workspaců/lakehouseů — anonymizuj ve slidech
3.12

Reusable assets pro budoucí použití

Po kurzu zůstanou tyto reusable artefakty:

  1. ovzdusi_demo workspace s prefilled lookups — naklonuj na další zakázky
  2. 3 Bronze ingest cesty — funkční šablony pro Notebook/DF Gen2/Pipeline (HTTP source)
  3. 2 stored procedures (silver, gold) — copy-paste do dalších DW projektů
  4. dim_station.csv (autoritativní z ČHMÚ registru), dim_pollutant.csv, dim_kraj.csv — statické lookup, version-controlled
  5. .pbix s Direct Lake — ukázka pro Power BI školení
⚠️
Důležité: CHMI URL je stabilní min. 5+ let. Demo bude fungovat opakovaně bez údržby. Pollutant kódy jsou EU standardní a stabilní. Bridge tabulka numeric ↔ EOI loc_code je area pro rozšíření do budoucna.