Marketing-Teams nutzen längst nicht mehr die vordefinierten Reports aus Google Analytics, sondern ihre eigenen, selbst geschriebenen Data Pipelines. 2026 besteht der moderne Marketing Data Stack aus drei Schichten: Raw Sources, Modeling Layer und Semantic Layer. Dieser Artikel erklärt, wie man diese drei Schichten mit dbt + BigQuery aufbaut, welche Fehler in welcher Phase auftreten und wie man eine produktionsreife, wartbare Struktur etabliert.
Source Mapping: Daten in BigQuery hochladen reicht nicht aus
Du hast GA4, Meta Ads und sGTM Events in BigQuery geladen – aber das ist erst der Anfang. Source Mapping bedeutet, Raw Tables in sinnvolle Contracts zu überführen. In dbt werden Source-Definitionen in .yml-Dateien verwaltet:
sources:
- name: raw_ga4
database: roibase-prod
schema: analytics_123456789
tables:
- name: events_*
identifier: events_*
loaded_at_field: event_timestamp
freshness:
warn_after: {count: 12, period: hour}
Diese Definition erfüllt drei Funktionen: (1) Data Lineage – welches Modell nutzt welche Raw Table, (2) Freshness Check – wenn das letzte Event älter als 12 Stunden ist, erscheint eine Warnung, (3) Contract – wenn die Spalte event_timestamp fehlt, schlägt der Build fehl.
Der häufigste Fehler: Raw Schema unverarbeitet verwenden. GA4's event_params als verschachteltes Array direkt in SQL benutzen – jede Query wird dann über 200 Zeilen lang. Im Source Mapping sollte die unnest-Logik zentral an einer Stelle leben:
-- models/staging/stg_ga4_events.sql
with source as (
select * from {{ source('raw_ga4', 'events_*') }}
),
flattened as (
select
event_date,
event_timestamp,
user_pseudo_id,
(select value.string_value from unnest(event_params) where key = 'session_id') as session_id,
(select value.int_value from unnest(event_params) where key = 'ga_session_number') as session_number
from source
)
select * from flattened
Dieses Modell wird nun downstream mit ref('stg_ga4_events') aufgerufen – die Raw event_params-Syntax ist upstream isoliert. Der Freshness Check läuft täglich, Schema-Änderungen werden automatisch als Fehler gemeldet.
Modeling Layer: Die Metrik einmal definieren, hundertfach nutzen
Nach der Staging-Schicht kommt die Modeling Layer. Hier werden Intermediate Models (Business Logic) und Mart Models (Aggregation) getrennt. Im Marketing Data Stack ist das kritischste Modell der Session → Transaction-Join:
-- models/marts/mrt_session_metrics.sql
with sessions as (
select * from {{ ref('int_sessions') }}
),
transactions as (
select * from {{ ref('int_transactions') }}
),
joined as (
select
s.session_id,
s.session_date,
s.traffic_source,
s.medium,
s.campaign,
t.transaction_id,
t.revenue,
t.transaction_timestamp
from sessions s
left join transactions t
on s.session_id = t.session_id
)
select
session_date,
traffic_source,
medium,
campaign,
count(distinct session_id) as sessions,
count(distinct transaction_id) as transactions,
sum(revenue) as total_revenue,
safe_divide(count(distinct transaction_id), count(distinct session_id)) as conversion_rate
from joined
group by 1, 2, 3, 4
Dieses Modell läuft täglich um 03:00 Uhr (dbt Cloud Scheduler), Looker Studio verbindet sich direkt mit dieser Table. Wenn eine Änderung notwendig ist, änderst du das SQL an einer Stelle – alle Dashboards aktualisieren sich automatisch.
Wichtiges Detail: safe_divide wird verwendet – wenn sessions = 0 ist, wird nicht durch Null dividiert, sondern null zurückgegeben. Exception Handling in der Production Pipeline passiert auf dieser Ebene.
dbt Tests: Datenqualität automatisch checken
Beim Definieren von Metriken in der Modeling Layer schreibst du gleichzeitig Tests:
# models/marts/schema.yml
models:
- name: mrt_session_metrics
columns:
- name: session_date
tests:
- not_null
- name: sessions
tests:
- not_null
- dbt_utils.expression_is_true:
expression: ">= 0"
- name: conversion_rate
tests:
- dbt_utils.expression_is_true:
expression: "<= 1"
Der dbt test Befehl führt diese Regeln aus. Wenn Conversion Rate > 1 herauskommt (= SQL-Fehler), schlägt der Build fehl und eine Alert geht an Slack. Statt manuellem QA gibt es hier automatisierte Datenqualität – der Rest des Data Stack baut auf diesem Fundament auf.
Semantic Layer: Definiere die Metrik, nicht die Query
Mit dbt v1.6+ ist die Semantic Layer aus dem Beta-Status heraus. Jetzt definierst du die Metrik nicht in SQL, sondern in einer .yml-Datei:
# models/semantic/metrics.yml
metrics:
- name: total_revenue
label: Total Revenue
model: ref('mrt_session_metrics')
type: sum
sql: total_revenue
timestamp: session_date
time_grains: [day, week, month]
- name: roas
label: Return on Ad Spend
type: ratio
numerator: total_revenue
denominator: total_ad_spend
Diese Definition wird an drei Orten verwendet: (1) Looker Studio, (2) Slack Bot über dbt Cloud Discovery API für Metrik-Anfragen, (3) Airflow DAG als Input für nachgelagerte ML Pipeline.
Vorteil: Metriken sind ohne SQL konsumierbar. Marketing Analyst schreibt jetzt „Show me ROAS by campaign, last 7 days" – dbt Semantic Layer kompiliert die Query automatisch. SQL-Logik sitzt im Model Layer, Metrik-Definition in der Semantic Layer – beide sind voneinander getrennt, Änderungen sind isoliert.
Achtung: Semantic Layer ist noch relativ neu – nicht alle BI-Tools haben native Integration. Im Roibase Production Stack nutzen wir einen Hybrid-Ansatz: kritische Metriken in der Semantic Layer, Custom Analysis über SQL Exposures.
Exposures: Downstream-Abhängigkeiten dokumentieren
Exposures zeigen, wo ein dbt Modell außerhalb von dbt verwendet wird:
# models/exposures.yml
exposures:
- name: looker_studio_performance_dashboard
type: dashboard
url: https://lookerstudio.google.com/...
depends_on:
- ref('mrt_session_metrics')
- ref('mrt_campaign_performance')
owner:
name: Marketing Analytics Team
email: [email protected]
Diese Definition wird in dbt docs visualisiert – welches Dashboard an welches Modell gekoppelt ist, wer zu informieren ist wenn sich ein Modell ändert. In Production ist klar, wenn man ein Breaking Change im Schema durchführt: Mit dbt run --select +mrt_session_metrics+ sieht man sofort die downstream-Auswirkungen.
Echtes Szenario: GA4 ändert den Key page_location zu page_url. Dank Exposure-Definition fanden wir sofort 3 betroffene Dashboards und 1 Airflow DAG. Die Migration war in 2 Stunden abgeschlossen. Ohne Exposures wären die Dashboards still kaputtgegangen – man hätte es erst durch User-Beschwerden erfahren.
Incremental Models: 2TB Daten nicht jeden Tag neu bauen
Bei Marketing Data können Daily Partitions terabytegroß werden. Nicht jeder dbt run Befehl kann ein Full Refresh sein – BigQuery Kosten und Laufzeit wären inakzeptabel. Stattdessen nutzt man Incremental Models:
-- models/marts/mrt_user_journey.sql
{{
config(
materialized='incremental',
partition_by={'field': 'event_date', 'data_type': 'date'},
cluster_by=['user_pseudo_id', 'traffic_source'],
incremental_strategy='insert_overwrite'
)
}}
select
event_date,
user_pseudo_id,
traffic_source,
-- ...
from {{ ref('stg_ga4_events') }}
{% if is_incremental() %}
where event_date >= date_sub(current_date(), interval 3 day)
{% endif %}
Diese Config macht drei Dinge: (1) Erstellt Partitionen in BigQuery – neue Tage werden hinzugefügt, alte Tage bleiben unberührt, (2) cluster_by verbessert Query-Performance, (3) insert_overwrite Strategie – die letzten 3 Tage werden gelöscht und neu geschrieben (für verspätet ankommende Daten).
Kostendifferenz: 365 Tage Daten, Full Refresh = 2,5 TB Scan ($12,50), Incremental = 3 GB Scan ($0,015). Bei einer täglich laufenden Pipeline über ein Jahr: ~$4.500 vs ~$5. Deshalb sind Incremental Models das Fundament eines Production Data Stack.
Den Data Stack mit Entscheidungsmechanismen verbinden
dbt + BigQuery bauen die Infrastruktur – aber der echte Wert liegt darin, Marketing-Entscheidungen zu beeinflussen. Ein typisches Szenario ist der Metrik-Flow von der Semantic Layer zu einem Slack Bot:
- Marketing Manager schreibt in Slack:
/metric roas last_30_days campaign=brand - Slack App ruft dbt Cloud Semantic Layer API auf
- API queried die Tabelle
mrt_session_metrics, berechnet ROAS - Ergebnis geht zurück an Slack: „Brand-Kampagne ROAS: 4,2x"
Für diesen Flow braucht man Semantic Layer + Custom Python Middleware. Im Roibase Production Stack zieht ein Airflow DAG täglich einen Snapshot aus der Semantic Layer – Looker Studio und interne Apps nutzen diesen Snapshot (keine API-Rate-Limit-Probleme).
Alternative: Im Service First-Party Daten & Mesarchitektur nutzen wir einen Hybrid Stack – dbt Semantic Layer + Cube.js. Cube.js fügt eine Caching-Schicht hinzu, verbessert BI-Performance. Die Wahl hängt von Data Volume und Query Pattern ab.
Production Checklist: Vor dbt Stack Deployment
dbt läuft lokal – vor Production sind folgende Kontrollen notwendig:
- CI/CD: Jeder Commit triggert
dbt build --select state:modified+über dbt Cloud oder GitHub Actions - Freshness Monitoring: Kritische Sources kriegen
warn_afterunderror_afterSchwellwerte - Alerting: dbt Cloud Webhooks → Slack Integration. Bei Build-Fehler: Team informiert in 5 Minuten
- Documentation:
dbt docs generateläuft automatisch, Artifacts gehen an S3/GCS - Cost Monitoring: BigQuery Slot Reservation oder On-Demand Cost Alert – bei unerwartetem Spike (z.B. $500/Tag Threshold) sofort benachrichtigt
- Backup Strategy: Production Data Warehouse mit Snapshot Tables – bei fehlerhaiter Update ist Rollback möglich
Kritischste Regel: In Production gibt es kein manuelles dbt run. Alle Executions laufen über Scheduler (dbt Cloud, Airflow, Prefect). Manuelles Ausführen zerstört Data Lineage – im Fehlerfall lässt sich Root Cause nicht feststellen.
dbt + BigQuery ist das Rückgrat des modernen Marketing Data Stack – Source Mapping bindet Raw Data an Contracts, Modeling Layer definiert Metriken an einer zentralen Stelle, Semantic Layer macht sie SQL-unabhängig konsumierbar. In Production machen Incremental Models und Test Coverage die Pipeline wartbar. Der nächste Layer: diese Daten mit Real-Time Activation verbinden – CDP, Audience Sync, Incrementality Measurement. Aber das ist eine andere Data Stack Diskussion.