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

RTI Library: architektura

Společné koncepty knihovny RTI dem: mental model, naming, Bronze → Silver → MV v KQL, Custom Endpoint
📡 Runbook · Real-Time Intelligence

Cíle kapitoly

  • Popsat mental model Fabric RTI: Eventhouse, KQL DB, Eventstream, konzumace
  • Dodržet naming conventions napříč šesti demy knihovny
  • Použít pattern Bronze → Silver (update policy) → materialized view v KQL
  • Postavit Custom Endpoint publisher a vyhnout se jeho anti-patternům
  • Odhadnout cost a zvládnout start/stop lifecycle workspace
15.1

Úvod

Tento dokument popisuje sdílené architektonické rozhodnutí, naming conventions, KQL patterns a anti-patterns používané ve všech 6 runbookech (A-F). Než pojedeš dělat individual runbook, projdi si tohle.

15.2

Mental model — jak vypadá Fabric RTI stack

SCHÉMA
┌──────────────────────────────────────────────────────────────────────┐
│                         FABRIC WORKSPACE                              │
│  ┌────────────────────────────────────────────────────────────────┐   │
│  │                       EVENTHOUSE                                │   │
│  │   ┌──────────────────────────────────────────────────────┐     │   │
│  │   │  KQL Database (je vlastně Azure Data Explorer DB)     │     │   │
│  │   │   ├── tables (raw + clean per demo)                   │     │   │
│  │   │   ├── update policies (raw → clean transform)         │     │   │
│  │   │   ├── materialized views (pre-aggregated state)       │     │   │
│  │   │   └── functions (reusable KQL snippets)               │     │   │
│  │   └──────────────────────────────────────────────────────┘     │   │
│  └────────────────────────────────────────────────────────────────┘   │
│                                ▲                                       │
│                                │ ingest                                │
│  ┌────────────────────────────────────────────────────────────────┐   │
│  │                      EVENTSTREAM(S)                             │   │
│  │   Source ────► Transformations ────► Destination                │   │
│  │   (input)        (filter, enrich)    (KQL DB / Lakehouse / etc) │   │
│  └────────────────────────────────────────────────────────────────┘   │
│                                ▲                                       │
│                                │ events                                │
│  ┌────────────────────────────────────────────────────────────────┐   │
│  │                      INPUT SOURCES                              │   │
│  │   • Sample Data (MS native, no setup)            → demo A       │   │
│  │   • Custom Endpoint (HTTP push)                  → demos B-E    │   │
│  │   • Azure Event Hub / IoT Hub / Kafka / CDC      → not used     │   │
│  └────────────────────────────────────────────────────────────────┘   │
│                                                                        │
│  ┌────────────────────────────────────────────────────────────────┐   │
│  │                      CONSUMPTION                                │   │
│  │   • Real-Time Dashboard (KQL native, sub-second refresh)        │   │
│  │   • Power BI (DirectQuery na KQL DB)                            │   │
│  │   • Activator (Reflex) → Teams/email/HTTP webhook               │   │
│  │   • Eventstream output back → Lakehouse (for batch reuse)       │   │
│  └────────────────────────────────────────────────────────────────┘   │
└──────────────────────────────────────────────────────────────────────┘

Kdy co použít

Use case Volba
Demo / proof-of-concept Sample Data → Eventstream preview, žádné destination
Real-time poll z public HTTP endpointu Notebook poller → Custom Endpoint → Eventstream → KQL DB
Real-time push z device/IoT Device pošle přímo na Custom Endpoint URL → Eventstream → KQL DB
Sub-sekundový dashboard Real-Time Dashboard nad KQL DB
Komplexní reporty s historií PBI DirectQuery na KQL DB + composite model s Direct Lake na Lakehouse gold
Alerting / automation Activator (Reflex) na KQL stream

Kdy NEpoužít Fabric RTI

  • Batch ETL — Lakehouse + Spark / Warehouse + T-SQL je levnější a expresivnější
  • Sub-millisekundové latency (high-frequency trading) — Fabric RTI cílí stovky ms, ne mikrosekundy
  • Strukturované transakce (CRUD na entity) — to je SQL DB / Cosmos use case
15.3

Naming conventions

Workspace

TEXT
DP600_Demo_RTI                    # main workspace pro celou library

Eventhouse / KQL DB

SCHÉMA
eh_rti_demo                       # single Eventhouse pro celou library
└── kql_rti_demo                  # single KQL DB

Eventstreams

TEXT
es_a_bicycles                     # demo A
es_b_chmi                         # demo B
es_c_ndic                         # demo C
es_d_alpr                         # demo D
es_e_pid                          # demo E

KQL tables — table prefix per demo

TEXT
a_bicycles_raw                    # demo A (pokud chceš sink)
b_chmi_raw         b_chmi_clean   # demo B
c_ndic_raw         c_ndic_clean
d_alpr_raw         d_alpr_clean
e_pid_raw          e_pid_clean

Materialized views

TEXT
mv_b_chmi_last_per_station        # latest reading per station
mv_c_ndic_avg_5min_per_loop       # 5-min avg counts per traffic loop
mv_d_alpr_count_per_camera_5min   # 5-min count per camera
mv_e_pid_active_vehicles          # currently active vehicles

Functions

TEXT
fn_kraj_from_idreg(idReg:int)     # lookup helper
fn_speed_class(kmh:int)           # bucketize

Notebooks

TEXT
nb_b_chmi_publisher               # publisher pro demo B
nb_c_ndic_publisher
nb_d_alpr_simulator
nb_e_pid_adapter

Real-Time Dashboards

TEXT
rtd_b_chmi_live
rtd_d_alpr_live

Power BI semantic models / reports

TEXT
sm_rti_combined                   # composite model nad celou KQL DB
rpt_b_chmi_realtime
rpt_d_alpr_realtime
15.4

Bronze → Silver → MV pattern v KQL

Stejný medallion princip jako v Days 1-2, ale realizovaný KQL nativnimi nástroji.

Bronze = raw table

  • Schema reflektuje to, co přijde z Eventstreamu
  • Žádné transformace, žádný join
  • Retention typicky 7-30 dní (krátká, raw je velký)
  • .create-merge table pattern (idempotentní)

Silver = clean table + update policy

  • Schema reflektuje business model (typed columns, parsed timestamps, joined dimensions)
  • Update policy = stored proc, která se spustí při každém ingestu do Bronze a transformovaná data inkrementálně přidá do Silver
  • Retention typicky 90-365 dní

MV = materialized view nad Silver

  • Pre-aggregated, denormalized state (např. last position per vehicle, 5-min avg per sensor)
  • Updatuje se inkrementálně, query ji vrátí v ms
  • Použij pro dashboardy a alert rules

Anti-patterns (don't do this)

⚠️
Pozor: Dotaz v update policy může číst jen běžné tabulky téže databáze — ne externaldata() ani external tables. Lookupy proto nejdřív materializujte (.set-or-replace dim_pollutant <| external_table('dim_pollutant_ext')). Pokud .alter table … policy update odmítne join kvůli streaming ingestion na zdrojové tabulce, vypněte ji: .alter table b_chmi_raw policy streamingingestion disable. Přesné podmínky ověřte v dokumentaci update policies.
  • ❌ Update policy z Silver do Gold (vícenásobný kaskádování → exploze cost) — radši MV
  • ❌ Jenom 1 tabulka pro raw + clean → ztratíš ability zpětně reprocesovat
  • ❌ MV bez lookback parametru → re-aggreguje VŠE při každém commit, devastující na cost
15.5

Custom Endpoint pattern (B/C/D/E)

V Microsoft Fabric (jaro 2026) neexistuje native HTTP poller v Eventstreamu. Pro real-time ingestion z external HTTP endpointů používáme tento pattern:

SCHÉMA
┌─────────────────┐                ┌──────────────────┐
│ External API    │                │ Fabric Notebook  │
│ (HTTP endpoint) │ ←── poll ────  │ (Pure Python)     │
└─────────────────┘                └──────────────────┘
                                            │
                                            │ POST JSON events
                                            ▼
                                   ┌──────────────────┐
                                   │ Eventstream      │
                                   │ Custom Endpoint  │ ← UNIQUE URL + KEY
                                   │ (HTTP push input)│
                                   └──────────────────┘
                                            │
                                            ▼
                                   ┌──────────────────┐
                                   │ KQL DB tables    │
                                   └──────────────────┘

Custom Endpoint setup

  1. V Eventstreamu: Add source → Custom App (pojmenování: Custom Endpoint nebo Custom App podle UI verze)
  2. Eventstream vyhradí unique ingestion URL + access key (zobrazí se v "Sample data" panel)
  3. Z Notebooku posíláš HTTP POST na ten URL
  4. Eventstream events okamžitě propíše do destination (KQL DB)

Notebook publisher boilerplate

PYTHON
import requests
import json
import time
from datetime import datetime, timezone

EVENTSTREAM_URL = "https://<your-region>.eventstream.fabric.microsoft.com/..."
EVENTSTREAM_KEY = mssparkutils.credentials.getSecret("kv-rti", "es-key-b")  # uložené v KeyVaultu

def publish_event(event: dict):
    """Send single event to Eventstream Custom Endpoint."""
    headers = {
        "Content-Type": "application/json",
        "Authorization": f"Bearer {EVENTSTREAM_KEY}",
    }
    r = requests.post(EVENTSTREAM_URL, headers=headers, data=json.dumps(event), timeout=10)
    r.raise_for_status()

def publish_batch(events: list[dict]):
    """Send batch (newline-delimited JSON)."""
    headers = {
        "Content-Type": "application/x-ndjson",
        "Authorization": f"Bearer {EVENTSTREAM_KEY}",
    }
    body = "\n".join(json.dumps(e) for e in events)
    r = requests.post(EVENTSTREAM_URL, headers=headers, data=body, timeout=30)
    r.raise_for_status()

Cost & latency notes

  • Eventstream charge je CU per event, ne per second. Pro F2 capacity je rozumný strop ~10k events/min sustained.

  • End-to-end latency Notebook → KQL = 2-10 sekund typicky.

  • Notebook poller na F2 má cold start 1-2 min — pro continuous run použij scheduled run, ne ad-hoc.

Anti-patterns

  • ❌ Notebook s while True: time.sleep(5) — sežere capacity i když nic nejede
  • ❌ Sending events 1-by-1 v loop bez batch — 10× horší throughput
  • ❌ Hardcoded Eventstream key v Notebook source — bezpečnostní problem
  • ❌ Žádný retry → první 5xx error a poller umře
15.6

KQL update policy template

KQL
// Step 1: Bronze table (Eventstream destination)
.create-merge table b_chmi_raw (
    EventEnqueuedUtcTime: datetime,
    idRegistration: int,
    startTime: datetime,
    idValueType: string,
    value: real
)

// Step 2: Silver table (typed, joined, business model)
.create-merge table b_chmi_clean (
    ingest_ts: datetime,
    measurement_ts: datetime,
    idRegistration: int,
    pollutant_code: string,
    pollutant_name: string,
    value: real,
    unit: string,
    eu_limit: real,
    over_limit: bool
)

// Step 3: Update policy function (transforms raw -> clean)
.create-or-alter function fn_b_chmi_transform() {
    b_chmi_raw
    | extend ingest_ts = EventEnqueuedUtcTime
    | extend measurement_ts = todatetime(startTime)
    // dim_pollutant = bezna KQL tabulka (lookup nahrany .set-or-replace, viz runbook B);
    // update policy nesmi cist externaldata ani external tables
    | join kind=leftouter dim_pollutant on $left.idValueType == $right.pollutant_code
    | extend over_limit = (value > eu_limit)
    | project ingest_ts, measurement_ts, idRegistration, pollutant_code, pollutant_name, value, unit, eu_limit, over_limit
}

// Step 4: Wire policy
.alter table b_chmi_clean policy update
    @'[{"IsEnabled":true,"Source":"b_chmi_raw","Query":"fn_b_chmi_transform()","IsTransactional":true,"PropagateIngestionProperties":true}]'

// Step 5: Materialized view nad Silver
.create-or-alter materialized-view with (backfill=true)
    mv_b_chmi_last_per_station on table b_chmi_clean {
        b_chmi_clean
        | summarize arg_max(measurement_ts, *) by idRegistration, pollutant_code
    }
15.7

Time-series KQL primer (pro Day 4 demo)

Top 5 funkcí, které musíš ukázat:

KQL
// 1. summarize bin() — equivalent SQL GROUP BY date_trunc
b_chmi_clean
| where measurement_ts > ago(24h)
| summarize avg(value) by bin(measurement_ts, 1h), pollutant_code

// 2. make-series — vyrovnaná time series s gaps
b_chmi_clean
| where pollutant_code == "PM10"
| make-series avg(value) on measurement_ts step 1h by idRegistration

// 3. arg_max / arg_min — latest / earliest per group
b_chmi_clean
| summarize arg_max(measurement_ts, *) by idRegistration

// 4. percentiles
b_chmi_clean
| summarize percentiles(value, 50, 95, 99) by pollutant_code

// 5. series_decompose / series_detect_anomalies
b_chmi_clean
| make-series val=avg(value) on measurement_ts step 1h
| extend (anomalies, score, _) = series_decompose_anomalies(val, 1.5)

KQL ↔ DAX/SQL analogie v KQL cheat sheetu (runbook 22).

15.8

Cost guidance

Aktivita Capacity Units (F2 baseline)
Eventstream idle 0 CU/s
Eventstream ingesting 1k events/min ~1-2 CU/s
KQL DB query (interactive) spike 1-5 CU/s, < 1s
Real-Time Dashboard auto-refresh (10s) sustained 0.5-1 CU/s per dashboard
Notebook publisher running spark cluster cost (cold start 1-2 min)
Activator alert evaluation < 0.5 CU/s

F2 (2 CU) je dostačující pro 1 demo running současně. Pro 2+ paralelně přepni na F4+ trial.

15.9

Workspace lifecycle (start/stop pattern)

Pokud F2 kapacita běží v start/stop módu:

  1. Před demo: start capacity (~30s), warmup query do KQL DB
  2. Eventstream + KQL se zaktivuje automaticky
  3. Po demu: stop capacity → veškerá data v KQL DB zůstanou (storage je perzistentní), ale ingestion zastaví
  4. Při restartu: Eventstream auto-resume od kde přestal (až do retention limitu)
15.10

Troubleshooting top 5

  1. „Events nepříchází do KQL" → check Eventstream "Data preview" tab. Pokud tam jsou events ale KQL prázdný, je problém v destination mapping nebo schema mismatch.
  2. „Update policy nezískává nic" → spusť update policy function ručně: fn_b_chmi_transform() | take 10. Pokud funkce vrátí data ale policy nic, problém v IsEnabled nebo schema mismatch.
  3. „Notebook poller selhává po 5 minutách" → spark session timeout. Použij Pure Python notebook (ne PySpark), který má vlastní lifecycle.
  4. „Real-Time Dashboard refresh laguje" → na F2 je sustained < 1k events/min. Při větším provozu přepni na F4+.
  5. „Custom Endpoint URL vrací 401" → zkontroluj že posíláš Authorization: Bearer <key>, ne ?key=... query param.
15.12

Setup KQL DB — pořadí skriptů a smoke test

Kompletní obsah souboru — rozbalte a zkopírujte tlačítkem Kopírovat.

Master index setupu (komentovaný)▼
KQL
// Master setup — poradi skriptu (KQL setup je v kazdem runbooku A-E)
// Master setup script — vytvoří všechny tabulky + update policies + MVs
// pro celou RTI Library v jednom průchodu.
//
// Idempotentní (`.create-merge`, `.create-or-alter`).
//
// Použití:
//   1. V Fabric workspace otevři KQL DB `kql_rti_demo`
//   2. New query → paste tento script → Run
//   3. Per demo (A-E): spust KQL setup skript z prislusneho runbooku
//
// POZN: KQL `.execute database` syntax pro chained scripts není jednoduchý.
// V praxi: zkopiruj KQL setup z runbooku 16-20 postupne.
// Tento file je jen index s pořadím:

// ──────────────────────────────────────────────────────────────────
// Order of execution
// ──────────────────────────────────────────────────────────────────
// 1. runbook 16 (A — Bicycles)      — 1 table
// 2. runbook 17 (B — CHMU polling) — 2 tables + 1 MV + 2 functions
// 3. runbook 18 (C — NDIC)         — 2 tables + 2 MVs + 2 functions
// 4. runbook 19 (D — Camera ALPR)  — 2 tables + 2 MVs + 1 function
// 5. runbook 20 (E — PID GTFS-RT)  — 2 tables + 2 MVs + 1 function
// (F nemá vlastní KQL — bolt-on Activator nad B/D)

// ──────────────────────────────────────────────────────────────────
// Verification queries (run after all 5 setups)
// ──────────────────────────────────────────────────────────────────

// Tables exist?
.show tables

// Update policies wired?
.show table b_chmi_clean policy update
.show table c_ndic_clean policy update
.show table d_alpr_clean policy update
.show table e_pid_clean policy update

// MVs exist?
.show materialized-views

// Functions exist?
.show functions

// ──────────────────────────────────────────────────────────────────
// Smoke test (run after publishers běží alespoň 5 min)
// ──────────────────────────────────────────────────────────────────

// A
// a_bicycles_raw | count

// B
// b_chmi_raw | count
// b_chmi_clean | count
// mv_b_chmi_last_per_station | count

// C
// c_ndic_raw | count
// c_ndic_clean | count

// D
// d_alpr_raw | count
// d_alpr_clean | count
// mv_d_alpr_speeders | count

// E
// e_pid_raw | count
// e_pid_clean | count
// mv_e_pid_active_vehicles | count