zum Blättern · Enter öffnet · Esc schließt

Kontakt aufnehmen.

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, Seed 20260804). 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

Streamlit Overview mit MoM-Kennzahlen: Umsatz, Ad-Anteil, MRR, ARPU und Revenue-Mix-Flächendiagramm
Overview im Streamlit-BI: Kennzahlen zum Monatsvergleich, darunter der Umsatzmix aus Ads und Abos.

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.

Dagster Orchestrierung 1. Extract Python Generierung und Aufnahme Parquet-Dateien 2. Transform dbt Bereinigung und Aggregation Warehouse-Tabellen (DuckDB) 3. Load Python / DuckDB Ergebnisexport CSV-Dateien 4. BI Streamlit / Tableau Analytik und Dashboards Steuerung Datenfluss
Der Datenweg. Dagster orchestriert Extract bis BI; die dicken Pfeile sind der Datenfluss (Parquet → DuckDB → CSV), die dünnen die Steuerung.

In der Praxis sieht derselbe Graph in der Dagster-UI so aus:

Dagster Global Asset Lineage: Raw über Staging und Intermediate zu Finance-, Growth- und Export-Assets
Global Asset Lineage: Raw → Staging → Intermediate → Core / Finance / Growth / Ops / Exports. Gemessen rund 42 Assets.

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
Raw-Entitäten und wohin sie rollen users 20.000 · 90 Tage sessions 332.808 pageviews ad_impressions subscriptions 280 payments 263 mart_revenue_daily Ads + Abos → Tagesumsatz plus Funnel-, Cohort-, DQ-Marts Partitionen in Europe/Berlin, Speicherung der Timestamps in UTC. Incremental-Staging liest mit Lookback - nicht nur „heute“. dbt-Build: PASS=225 · WARN=1 · ERROR=0 - die 225 umfassen Modellbau und Tests; darunter ~192 Data-Tests.
Das Domänenmodell ist bewusst schlicht: genug, um Umsatz, Funnel und DQ zu verbinden - zu wenig, um sich in Legacy-Komplexität zu verlieren.

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.

Fünf Störungen, fünf Lektionen - alle lokal reproduzierbar INC-001 Late Arrivals Late-Rate 0 → 2,9 % Lookback = 3 Tage TTD ≈ 3,4 s TTR ≈ 48 s INC-002 Schema-Drift neue Spalte + Komma union_by_name TTD ≈ 5 s TTR ≈ 25 s INC-003 Outage + Backfill 5 Tage leer 111.013 Zeilen TTD ≈ 0,2 s TTR ≈ 29 s INC-004 Stille Währung strukturell grün manuelle Semantik TTD ≈ 15 s TTR ≈ 80 s INC-005 Definition 1 → 3 Sessions -13 % Active Users TTD = 0 (geplant) TTR ≈ 22 s INC-003 ist der visuelle Höhepunkt: rote Checks, leere Partitionen, danach ein 30-Tage-Backfill mit grünem Raster. INC-004 ist die unbequemste Lektion: not_null und unique bleiben grün, während die Schweiz „reich“ aussieht. Zeiten: Laptop-Demo auf Profil small - Details und Methodik im Incident-Runbook.
Die fünf Fälle decken genau die Themen ab, die in Data-Engineering-Stellenanzeigen oft als Stichworte stehen - hier mit messbarem Nachweis statt Folien.

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.

Dagster Asset Checks für raw/ad_impressions: raw_ad_impressions_nonempty Passed auf Partition 2026-07-15
Asset Checks am Raw-Asset: nonempty, Freshness, Anomalien - sichtbar in derselben UI wie die Lineage.

Baseline vor dem Löschen: 111.013 Zeilen, Umsatz 482,91 €. Nach Wiederherstellen der fünf Tage und einem 30-Tage-Rebuild (2026-07-012026-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:

Dagster-Backfill-Raster: dreißig grüne Partitionen vom 1. bis 30. Juli nach erfolgreicher Wiederherstellung
Nach dem Outage: dreißig Partitionen grün. Das Standbild der Demo - Screencast-Storyboard liegt im Repository, MP4 ist optionaler Feinschliff.

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:

Streamlit Data-Quality-Seite mit DQ-Mart, Incident-Markern auf der Zeitleiste und Tabelle der täglichen Qualitätskennzahlen
Data Quality als Operator-Oberfläche: Timeline mit Incident-Markern, Tabelle, Callout - nicht nur ein grüner CI-Haken.
Asset Checks: Raw-Verträge und Business-Assertions Am Raw (ERROR / WARN) nonempty · freshness · row-count z-score schema_drift · null user_id · duplicate rate hourly χ² · late_arrival_rate Idee Partition existiert, ist frisch, Schema passt, Volumen und Tagesprofil wirken plausibel Am Business (ERROR) revenue_reconciliation (|ad+sub-total| < 0,01 €) funnel_monotonicity (activated ≤ signups) Idee Selbst bei grünem Raw kann die Semantik lügen - INC-004 ist der Beweis. Deshalb beide Ebenen. CI auf GitHub Actions: lint → generate → dbt build → pytest → docs - self-contained, ohne externe Services. Generator-Pytest-Coverage 71 % (Gate ≥ 70 %).
Checks am Asset, nicht als nachgelagerter Report. Wer Lineage öffnet, sieht denselben Vertrag, den der nächtliche Lauf prüft.

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:

Streamlit Revenue: 5.813 € Allzeit-Umsatz, Ads versus Abos, Aufschlüsselung nach Surface, Slot und Land
Revenue: der Mix, den Finance täglich erwartet - und der bei Late Arrivals oder Outages als Erstes „weich“ wird.

Growth zeigt Funnel, Cohort-Heatmap und MRR-Waterfall:

Streamlit Growth: Funnel, Cohort-Heatmap und MRR-Waterfall
Growth: Funnel und Kohorten - dieselben Marts, die INC-005 neu berechnet, wenn sich „aktiv“ ändert.

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.

← Alle Projekte