Projekte
Fünf Incidents, eine Pipeline: Analytics für ein Freemium-Portal.
Demo5. August 2026 · aktualisiert 5. August 2026
- Bereich
- SaaS
- Umsetzung
- 4 Wochen
- Kompetenz
- Data Engineering · Business Intelligence
- Stack
- Python · Polars · DuckDB · dbt-duckdb · Dagster · Streamlit · Altair · Git
Ergebnis
- End-to-End von Hive-Parquet über dbt/DuckDB und Dagster bis zum Streamlit-Operator-Dashboard
- Zehn Asset Checks und fünf reproduzierbare Incidents mit gemessenen Zeiten
- dbt-Build: 225 PASS inkl. Modelle und Tests (davon ~192 Data-Tests), WARN=1, ERROR=0
- Outage von fünf Tagen: 111.013 Zeilen und 482,91 € Umsatz vollständig wiederhergestellt
Dieser Fall ist bewusst kein Kundenprojekt, sondern eine offene Demonstration: ein juristisch neutrales Freemium-Portal, vollständig synthetische Events, ein Repository, in das man hineinschauen kann. Interessant daran ist nicht, dass noch ein ETL-Tutorial mit dbt gelingt - sondern dass die Pipeline die Alltagsstörungen überlebt, die in echten Betrieben den Umsatzbericht kaputt machen.
Late Arrivals, Schema-Drift, ein mehrtägiger Ingest-Ausfall, stille Währungskorruption und eine Definitionsänderung an Active Users: fünf Incidents, jeweils als Skript reproduzierbar, mit gemessenen Zeiten und Zahlen. Darunter sitzt eine schlanke, aber produktionsnahe Plattform - Generator, DuckDB, Dagster, Streamlit - die genau diese Spannungen adressiert.
Alle Daten sind synthetisch (Profil
small, Seed20260804). Es gibt keine echten Nutzer- oder Unternehmensdaten. Ähnlichkeiten mit konkreten Portalen sind rein fachlich-analog. Quellcode und Runbook: portal-analytics-platform.
Die Ausgangslage
Freemium-Kommunikationsportale - Webmail, News, Cloud-Speicher - finanzieren sich über Display-Werbung und Premium-Abos. Finance will den Umsatzmix, Growth den Funnel, Ops die Datenqualität. Alle drei erwarten tägliche Aussagen.
Die Realität darunter ist unordentlich. Partner-Feeds kommen verspätet. Schemas ändern sich ohne Ankündigung. Manchmal fehlt ein ganzer Ingest-Tag. Ein Feld, das gestern noch EUR war, trägt heute einen Betrag, der wie CHF aussieht. Und irgendwann ändert Product die Definition von „aktivem Nutzer“ - die Historie muss mit.
Wer nur „dbt run succeeded“ feiert, sieht diese Fälle nicht. Wer eine Pipeline betreibt, schon. Genau diese Lücke sollte der Demo-Fall schließen: nicht ein weiteres Hello-World-Warehouse, sondern eine Oberfläche, auf der ein Operator einen Ausfall erkennt, einen Backfill startet und danach dieselben Zahlen wiederfindet.
Was daraus wurde
Auf dem gemessenen small-Profil liegen rund 1,83 Mio. Ad Impressions,
1,50 Mio. Pageviews und 20.000 Nutzer über neunzig Tage
(2026-05-03 bis 2026-07-31). Der Allzeit-Umsatz im Demo-Fenster liegt bei
etwa 5.813 €, der Ad-Anteil um die 80 Prozent, die Free-to-Paid-Conversion
bei rund 1,2 Prozent.
Vier Streamlit-Seiten - Overview, Revenue, Growth, Data Quality - lesen das Warehouse ausschließlich read-only, damit parallele Writer (Dagster/dbt) nicht kollidieren. CSV-Extrakte liegen daneben für Tools, die man nicht live gegen DuckDB hängen will.
Architektur
Der Datenweg ist absichtlich kurz und vollständig lokal lauffähig. Phase 1 braucht keine Cloud-Dienste; Phase 2 (MinIO, Iceberg, Trino) ist vorbereitet, aber bewusst zurückgestellt.
In der Praxis sieht derselbe Graph in der Dagster-UI so aus:
Zwei Asset-Sets teilen die Arbeit: partitionierte Incrementals (Staging und
ähnliche Modelle) bekommen von Dagster start_date / end_date als dbt-Vars;
nicht partitionierte Full-Refresh-Marts leben in einem zweiten @dbt_assets
ohne partitions_def. So erzwingt man keine Fake-Tagespartitionen auf Tabellen,
die gar nicht tagweise geschnitten sind - und Backfills bleiben dort sinnvoll,
wo sie hingehören.
DuckDB kennt nur einen Writer. Deshalb steht im Dagster-Pool
default_limit: 1. Streamlit öffnet die Datei read-only. Für ein Laptop-Demo
ist das richtig; für parallele Produktion wäre es falsch - deshalb ist
Trino/Iceberg der geplante nächste Schritt, nicht eine nachträgliche Entschuldigung.
Das Datenmodell
Sechs Raw-Entitäten landen als Hive-Partitionen. Gemessenes small-Profil:
| Entität | Zeilen | Dateien | Größe |
|---|---|---|---|
users |
20.000 | 90 | 0,54 MB |
sessions |
332.808 | 90 | 15,4 MB |
pageviews |
1.497.187 | 90 | 61,5 MB |
ad_impressions |
1.831.337 | 90 | 88,0 MB |
subscriptions |
280 | 67 | 0,31 MB |
payments |
263 | 64 | 0,31 MB |
Fünf Incidents
Jedes Szenario läuft über .\task.ps1 incident -Number N: Defekt injizieren,
Erkennung messen, Fix anwenden, saubere Partitionen wiederherstellen. Die
Zahlen unten stammen aus dem dokumentierten Lauf vom 2026-08-04 (Profil
small, Seed 20260804) und sind im offenen
Incident-Runbook
nachzulesen - keine erfundenen Metriken.
Zu TTD und TTR: Die Labels sind bewusst grob. Time-to-detect und Time-to-recover messen hier die Dauer des Demo-Skripts auf dem Laptop, nicht eine Produktions-SLA. Bei INC-001 bis INC-003 greifen Asset Checks bzw. lokale Messungen; bei INC-004 ist die „Erkennung“ ein manueller semantischer Vergleich (strukturelle Tests bleiben grün); bei INC-005 ist TTD null, weil die Änderung geplant ist - gemessen wird nur die Neuberechnung.
INC-001 - Verspätete Events
Symptom: late_arrival_rate steigt auf den Raw-Ad-Impressions; der Umsatz der
letzten Tage wirkt „weich“. Der Defekt schreibt etwa drei Prozent der Zeilen so
um, dass event_ts auf dem Vortag liegt, die Hive-Partition aber „heute“ bleibt.
Ein naiver Incremental-Filter where event_date = max(event_date) sieht den
Vortag nie wieder.
Gemessen auf Partition 2026-07-30: Late-Rate 0 → 2,9 %. TTD ≈ 3,4 s
(Check plus lokale Messung), TTR ≈ 48 s.
Die Staging-Modelle nutzen deshalb delete+insert plus ein Makro
late_arrival_incremental mit Lookback von drei Tagen. Partitionstag und
„welche Zeilen gehören dazu“ sind zwei verschiedene Dinge - wer das verwechselt,
unterzählt dauerhaft Umsatz.
INC-002 - Schema-Drift
Symptom: Ingestion legt experiment_variant dazu und speichert ecpm_eur als
europäische Kommazahl ("1,42"). Ohne Schutz scheitert der Cast - oder der
Umsatz wird still null.
Der Check raw_schema_drift meldet extra=experiment_variant. Staging
überlebt trotzdem: union_by_name=true und ein safe_decimal-Makro
(replace(',', '.') + try_cast). Drift wird also überlebt und gemeldet,
nicht verschluckt. TTD ≈ 5 s (Schema-Check), TTR ≈ 25 s.
INC-003 - Ingest-Ausfall und Backfill
Fünf aufeinanderfolgende Tage ohne Ad-Impression-Dateien. Die Checks
raw_ad_impressions_nonempty, raw_freshness und raw_row_count_anomaly
schlagen an. Der Dashboard-Umsatz für diese Tage bricht ein.
Baseline vor dem Löschen: 111.013 Zeilen, Umsatz 482,91 €. Nach
Wiederherstellen der fünf Tage und einem 30-Tage-Rebuild
(2026-07-01 … 2026-07-30) kommen genau diese Zahlen zurück. TTD ≈ 0,2 s
(Dateisystem plus Checks), TTR ≈ 29 s auf dem Profil small - in der UI ist
der Backfill das Bild, das man zeigt:
Die Regel danach ist simpel und unbequem: Raw reparieren, dann Staging und Marts mit explizitem Fenster neu bauen. Marts „flicken“, ohne Raw zu heilen, ist keine Recovery.
INC-004 - Stille Währungskorruption
Hier scheitert nichts Strukturelles. CHF-Zahlungen werden so geschrieben, als
wäre der Betrag noch nicht nach EUR umgerechnet: amount_eur * (1/0,95). Auf
einer einzelnen Zeile sind das grob +5 %; not_null und unique bleiben
grün. Menschen merken Wochen später: „Die Schweiz sieht reich aus.“
Im Generator-Profil small gibt es im Juli nur neun CHF-Zahlungen. Deshalb
bewegt sich der CHF-Durchschnitt nur von 3,515 auf 3,567 (+1,5 %), während
EUR bei ~3,734 bleibt: der Defekt trifft wenige Zeilen, der Mittelwert über alle
CHF-Zahlungen dämpft den Effekt. Der Punkt ist nicht die Größe des Fehlers -
sondern dass rein strukturelle Tests ihn nicht sehen. Semantische Assertions
(assert_chf_amounts_look_converted) und Monitoring CHF vs. EUR gehören dazu;
Severity-Politik trennt ERROR (Verträge) von WARN (semantische Hinweise), damit
CI nicht bei jedem Rampen-Artefakt blockiert.
TTD ≈ 15 s (manuelle semantische Abfrage, nicht ein grüner CI-Haken), TTR ≈ 80 s (Monat neu erzeugen plus Staging-Refresh).
INC-005 - Historische Neuberechnung
Product ändert „aktiver Nutzer“ von ≥1 Session/Tag auf ≥3 Sessions/Tag. ARPU und Active-User-Kacheln müssen die Historie nachziehen - und Stakeholder brauchen vorher eine klare Botschaft, nicht eine stille Verschiebung.
Baseline mit Schwelle 1: 19.065 aktive Nutzer. Nach Full-Refresh mit
Schwelle 3: 16.576 (-2.489, -13 %). Die Neuberechnung selbst braucht auf
dem Profil small ≈ 11 s; inklusive Zurücksetzen der Demo-Defaults liegt TTR
bei ≈ 22 s. TTD ist 0, weil die Änderung geplant ist - das Risiko liegt in
der Kommunikation, nicht in einem fehlgeschlagenen Check. Die Variable
active_user_min_sessions steuert die Definition; der Default bleibt 1, bis
Product unterschreibt. In Produktion würde man achtzehn Monate nicht in einem
Lauf pressen, sondern monatsweise mit Fortschrittsmeldung.
Betrieb und Datenqualität
Zehn Dagster Asset Checks decken Freshness, Leere Partitionen, Row-Count-Z-Score,
Schema-Drift, Null-/Duplikatraten, stündliche χ²-Verschiebung,
Revenue-Reconciliation und Funnel-Monotonie ab. Die Streamlit-Seite
Data Quality visualisiert mart_data_quality_daily inklusive Marker für das
Outage-Fenster - genau die Ansicht, die den Betriebsanteil greifbar macht:
BI: Umsatz und Wachstum
Zwei Fachseiten neben Overview und DQ. Revenue zeigt Allzeit-Umsatz, Mix Ads vs. Abo, Aufschlüsselung nach Surface, Slot und Land sowie eCPM:
Growth zeigt Funnel, Cohort-Heatmap und MRR-Waterfall:
Formate sind DACH-tauglich (EUR, de-DE), Partitionen in Europe/Berlin.
Tableau live gegen DuckDB entfällt bewusst: Streamlit live plus CSV-Extrakt ist
der ehrliche Laptop-Weg; ein eingebettetes Tableau würde Cookies setzen und das
Versprechen dieser Website brechen.
Entscheidungen und Trade-offs
Neun ADRs dokumentieren, warum bestimmte Wege gewählt wurden - und welche bewusst nicht.
Dagster statt Airflow, weil Assets, Checks und Lineage in einer UI das Portfolio-Signal sind; Airflow bleibt der Industrie-Default, braucht hier aber mehr Boilerplate für denselben Vertrag.
Lookback statt reinem Watermark, weil Partner-Batches und Offline-Clients sonst dauerhaft unterzählen - INC-001 ist der Nachweis.
DuckDB jetzt, Trino später: Parität der Modelle über Makros geplant; Impala wird nicht deployed, Trino ist der vorgesehene Nachfolger. Iceberg statt dauerhaftem Hive-Raw ist Phase 2.
Test-Severity: Schlüssel, Nullen, Revenue-Identität → ERROR. Semantische Warnungen wie „keine Ads auf Ad-free-Plänen“ → WARN, weil Same-Day-Conversion sonst CI blockiert, ohne dass der Vertrag gebrochen wäre.
Was ich anders machen würde: INC-003 als MP4 statt nur Standbild; INC-004 auf
dem Profil medium härter machen, damit der semantische Test zuverlässig
fehlschlägt; das Profil large (~10 GB) bleibt gestrichen - small reicht für
CI, medium für dichtere Demos. Die SQL-Server-Legacy-Fallstudie (M8) ist
Platzhalter: dasselbe Domänenmodell soll später gegen ein langsames
Reporting-SQL gespiegelt werden, ohne den heutigen Quickstart zu belasten.
Ergebnis
- Ad Impressions (Profil
small): ~1,83 Mio. - Pageviews: ~1,50 Mio.
- Nutzer / Zeitraum: 20.000 · 90 Tage
- dbt-Build: PASS=225 · WARN=1 · ERROR=0 (225 = Modellbau + Tests; darunter ~192 Data-Tests)
- Asset Checks: 10 (Raw + Business)
- INC-003 Recovery: 111.013 Zeilen · 482,91 €
- INC-005 Active Users: -13 % (19.065 → 16.576)
- Generator-Tests: 71 % Coverage
Der Unterschied zu einem typischen „dbt + Dashboard“-Portfolio-Stück steckt
nicht in der Tech-Liste. Er steckt darin, dass man fünf Störungen starten, sehen
und heilen kann - und danach dieselben Zahlen wiederhat. Wer das Repository
klont und .\task.ps1 incident -Number 3 ausführt, bekommt denselben Outage,
nicht einen ähnlichen.