dbt-Modelle: Leitfaden zu Datentransformationen
dbt (data build tool) ist zum Standardwerkzeug des Analytics Engineering geworden, und das dbt-Modell ist sein grundlegender Baustein. Im Kern ist ein dbt-Modell nichts weiter als eine einzelne SELECT-Anweisung, gespeichert in einer Datei mit der Endung .sql. dbt nimmt dieses SELECT, umhüllt es mit dem passenden DDL und materialisiert es in Ihrem Data Warehouse oder Lakehouse als Tabelle, View oder inkrementelle Tabelle. Diese täuschend einfache Idee erschließt einen vollständigen Arbeitsablauf der Softwareentwicklung, also Versionsverwaltung, Tests, Dokumentation und Verwaltung von Abhängigkeiten, auf gewöhnlichem SQL, das Analyseteams ohnehin beherrschen.
Ein dbt-Modell ist eine Datei mit der Endung .sql und einer einzelnen SELECT-Anweisung. dbt übersetzt sie in Anweisungen vom Typ CREATE TABLE/VIEW und führt sie in der Reihenfolge der Abhängigkeiten aus, abgeleitet aus den Aufrufen von ref() und source(). Modelle werden in die Schichten Staging, Intermediate und Mart gegliedert, und jeder Lauf erzeugt maschinenlesbare Metadaten (manifest.json), die Plattformen für Data Governance wie Dawiso speisen.
Was ist dbt?
dbt ist ein quelloffenes Werkzeug für die Kommandozeile, daneben mit dbt Cloud auch ein verwalteter Dienst, und kümmert sich um das T in ELT, also um die Transformation von Daten, die bereits in Ihr Warehouse geladen wurden. Statt komplexe gespeicherte Prozeduren oder brüchige eigene Skripte zu pflegen, lässt dbt Ihr Team SELECT-Anweisungen schreiben und Abhängigkeiten zwischen ihnen ausdrücken. dbt übersetzt diese Anweisungen, löst den Graphen der Abhängigkeiten auf und führt die Transformationen in der richtigen Reihenfolge gegen Ihre Zieldatenbank aus (Snowflake, BigQuery, Databricks, Redshift, DuckDB und viele weitere).
Wichtige Fähigkeiten, die dbt in die Arbeit an Transformationen einbringt:
- Selbsttätige Auflösung des DAG (gerichteter azyklischer Graph) aus den Aufrufen von
ref()undsource() - Eingebautes Framework für Tests mit Zusicherungen zur Datenqualität
- Erzeugte Dokumentation mit interaktiven Graphen der Lineage
- Jinja-Vorlagen für SQL nach dem DRY-Prinzip (Makros, Bedingungen, Schleifen)
- Ein wachsendes Ökosystem an Paketen aus der Gemeinschaft (dbt_utils, dbt_expectations, AutomateDV)
Weil die gesamte Logik der Transformation in Dateien mit den Endungen .sql und .yml liegt, fügt sich dbt natürlich in Git ein und ermöglicht Code-Review, Branches, CI/CD und einen vollständigen Audit-Trail jeder Änderung.
Was ist ein dbt-Modell?
Ein dbt-Modell ist eine Datei mit der Endung .sql, die genau eine SELECT-Anweisung enthält. Der Dateiname wird zum Namen des Modells und damit zum Namen des Objekts, das dbt in Ihrem Warehouse anlegt.
-- models/staging/stg_orders.sql
select
order_id,
customer_id,
status,
cast(created_at as timestamp) as created_at,
amount_cents / 100.0 as amount
from {{ source('raw', 'orders') }}
Wenn Sie dbt run ausführen, übersetzt dbt dies in eine Anweisung CREATE TABLE AS oder CREATE VIEW AS, je nach Materialisierung, und führt sie aus. Die Syntax mit den doppelten geschweiften Klammern ist Jinja-Templating: Sie lässt dbt zur Übersetzungszeit die richtigen Schemanamen einsetzen, Makros auswerten und Referenzen auflösen.
Modelle können wahlweise mit einem Jinja-Konfigurationsblock beginnen, der die Konfiguration auf Modellebene setzt:
-- models/marts/fct_orders.sql
{{
config(
materialized = 'incremental',
unique_key = 'order_id',
on_schema_change = 'sync_all_columns'
)
}}
select
o.order_id,
o.customer_id,
c.country,
o.amount,
o.created_at
from {{ ref('stg_orders') }} as o
left join {{ ref('stg_customers') }} as c using (customer_id)
{% if is_incremental() %}
where o.created_at > (select max(created_at) from {{ this }})
{% endif %}
Die Funktionen ref() und source()
Zwei Jinja-Funktionen sind für jedes dbt-Projekt zentral: ref() und source().
ref(): Verweise auf andere Modelle
{{ ref('model_name') }} teilt dbt mit, dass das aktuelle Modell von einem anderen Modell abhängt. Zur Übersetzungszeit ersetzt dbt den Aufruf durch den vollständig qualifizierten Namen aus Schema und Tabelle für die jeweilige Umgebung. Dabei geschehen zwei entscheidende Dinge:
- Verfolgung der Abhängigkeiten: dbt fügt eine Kante in den DAG ein und stellt so sicher, dass das referenzierte Modell zuerst gebaut wird
- Übertragbarkeit zwischen Umgebungen: Der aufgelöste Name wechselt selbsttätig zwischen den Schemata für Entwicklung, Staging und Produktion, ganz ohne fest verdrahtete Schemanamen
source(): Verweise auf Rohdaten
{{ source('source_name', 'table_name') }} verweist auf Rohtabellen, die nicht von dbt erzeugt wurden. Sources werden in Dateien mit der Endung .yml deklariert:
# models/staging/_sources.yml
version: 2
sources:
- name: raw
database: analytics
schema: raw_data
tables:
- name: orders
description: "Raw orders from the transactional database"
loaded_at_field: _loaded_at
freshness:
warn_after: {count: 12, period: hour}
error_after: {count: 24, period: hour}
- name: customers
description: "Raw customer records"
Deklarierte Sources erschließen Prüfungen der Aktualität (dbt source freshness), die Verfolgung der Lineage zurück bis zu den Rohtabellen und die Dokumentation der Ladeschicht neben den Modellen der Transformation.
Materialisierungen von Modellen
Eine Materialisierung legt fest, wie dbt das Ergebnis des SELECT eines Modells im Warehouse ablegt. dbt bringt vier eingebaute Materialisierungen mit.
View
Die leichteste Möglichkeit: dbt legt eine View in der Datenbank an. Es werden keine Daten gespeichert, denn Abfragen gegen die View führen stets das darunterliegende SQL erneut aus. Views sind die voreingestellte Materialisierung.
- Am besten für: Staging-Modelle, einfache Transformationen, selten abgefragte Modelle
- Abwägung: Die Antwortzeit entspricht den Kosten dafür, das SELECT jedes Mal neu auszuführen
Table
dbt verwirft bei jedem Lauf die physische Tabelle und legt sie neu an. Alle Zeilen werden von Grund auf neu berechnet.
- Am besten für: Modelle der Mart-Schicht, mittelgroße Datenbestände, bei denen eine vollständige Neuberechnung vertretbar ist
- Abwägung: Bauzeit und Rechenaufwand wachsen mit der Datenmenge, und Teilaktualisierungen gibt es nicht
Incremental
Beim ersten Lauf baut dbt die vollständige Tabelle. Bei den folgenden Läufen verarbeitet es nur neue oder geänderte Zeilen und führt sie in die bestehende Tabelle ein.
{{
config(
materialized = 'incremental',
unique_key = 'event_id',
incremental_strategy = 'merge'
)
}}
select
event_id,
user_id,
event_type,
occurred_at
from {{ source('raw', 'events') }}
{% if is_incremental() %}
where occurred_at > (select max(occurred_at) from {{ this }})
{% endif %}
- Am besten für: große Faktentabellen, Ereignisströme, überwiegend anhängende Datenbestände
- Abwägung: aufwendigere Logik, und falsche Filterbedingungen können zu fehlenden Datensätzen oder zu Dubletten führen
Ephemeral
Ephemere Modelle werden überhaupt nicht materialisiert. dbt fügt ihr SQL als Common Table Expression (CTE) in jedes nachgelagerte Modell ein, das auf sie verweist.
- Am besten für: Zwischenlogik für Berechnungen, die Sie wiederverwenden wollen, ohne sie zu speichern
- Abwägung: nicht unmittelbar abfragbar, dazu aufgeblähte CTEs in komplexen nachgelagerten Modellen
Gliederung der Modelle: Staging, Intermediate und Marts
Das am weitesten verbreitete Muster für dbt-Projekte gliedert die Modelle in drei Schichten und hält die Logik der Transformation dadurch modular, prüfbar und nachvollziehbar.
Die Schicht Staging (models/staging/)
Ein Staging-Modell je Quelltabelle. Aufgabe eines Staging-Modells ist es, Rohdaten zu säubern und zu vereinheitlichen, also Spalten umzubenennen, Datentypen zu wandeln, Nullwerte zu behandeln und leichte fachliche Regeln anzuwenden. Staging-Modelle verweisen ausschließlich auf source(), nie auf andere Modelle. Üblicherweise werden sie als Views materialisiert.
-- models/staging/stg_customers.sql
select
id as customer_id,
lower(trim(email)) as email,
first_name,
last_name,
cast(created_at as timestamp) as created_at,
is_active
from {{ source('raw', 'customers') }}
Die Schicht Intermediate (models/intermediate/)
Modelle der Schicht Intermediate verbinden Staging-Modelle und reichern sie an. Sie halten fachliche Logik fest, die mehrere Marts teilen, die aber noch nicht unmittelbar veröffentlicht werden soll. Sie verweisen per ref() auf Staging-Modelle oder auf andere Modelle dieser Schicht.
-- models/intermediate/int_orders_enriched.sql
select
o.order_id,
o.amount,
o.created_at,
c.customer_id,
c.email,
c.country,
p.product_name,
p.category
from {{ ref('stg_orders') }} as o
join {{ ref('stg_customers') }} as c using (customer_id)
join {{ ref('stg_products') }} as p using (product_id)
Die Schicht Marts (models/marts/)
Marts sind die abschließenden, für die Auswertung bereiten Ergebnisse, gegliedert nach fachlicher Domäne (Finanzen, Marketing, Produkt). Sie stellen saubere, breite Tabellen bereit, die für Werkzeuge der BI und für freie Abfragen optimiert sind. Üblicherweise werden sie als Tabellen oder als inkrementelle Modelle materialisiert.
-- models/marts/finance/fct_revenue.sql
select
date_trunc('month', created_at) as month,
country,
category,
sum(amount) as revenue,
count(distinct order_id) as order_count
from {{ ref('int_orders_enriched') }}
group by 1, 2, 3
Namenskonventionen
Einheitliche Benennung gehört zu den wirksamsten Praktiken in dbt-Projekten. Die verbreitete Konvention nutzt Präfixe, um Schicht und Art eines Modells anzuzeigen:
stg_: Staging-Modelle, eines je Quelltabelle:stg_orders,stg_customersint_: Modelle der Schicht Intermediate:int_orders_enriched,int_session_attributionfct_: Faktentabellen, transaktional und mit additiven Kennzahlen:fct_orders,fct_page_viewsdim_: Dimensionstabellen mit beschreibenden Merkmalen:dim_customers,dim_productsrpt_: Modelle der Berichtsschicht für bestimmte Anwendungsfälle in der BI, eine wahlweise zusätzliche Schicht
Dokumentation mit schema.yml
dbt erzeugt eine durchsuchbare Seite mit Dokumentation, und zwar aus Beschreibungsdateien mit der Endung .yml, die neben den Modellen liegen. Jedes Modell und jede Spalte kann eine Beschreibung tragen, die neben den selbsttätig abgeleiteten Graphen der Lineage erscheint.
# models/marts/finance/_finance.yml
version: 2
models:
- name: fct_revenue
description: >
Monthly revenue aggregated by country and product category.
Primary source of truth for the Finance dashboard.
columns:
- name: month
description: "First day of the calendar month (UTC)."
- name: revenue
description: "Sum of order amounts in USD for the period."
tests:
- not_null
- name: order_count
description: "Distinct count of orders contributing to revenue."
tests:
- not_null
Der Leitfaden zu dbt-Tests zeigt ausführlich, wie Zusicherungen zur Qualität in schema.yml eingerichtet werden.
Modelle ausführen
Die wichtigsten Befehle für die Arbeit mit Modellen:
# Run all models
dbt run
# Run a specific model
dbt run --select stg_orders
# Run a model and all its upstream dependencies
dbt run --select +fct_revenue
# Run a model and all its downstream dependents
dbt run --select fct_revenue+
# Build models + run tests in one command (recommended for CI)
dbt build --select +fct_revenue
dbt build ist der empfohlene Befehl für Strecken der CI/CD: Er führt Modelle, Seeds, Snapshots und Tests in der Reihenfolge der Abhängigkeiten aus und bricht früh ab, wenn ein Test fehlschlägt, noch bevor nachgelagerte Modelle gebaut werden.
Lineage und Governance
Einer der am meisten unterschätzten Züge von dbt sind die maschinenlesbaren Metadaten, die als Nebenprodukt der gewöhnlichen Entwicklungsarbeit entstehen. Jeder dbt run erzeugt eine manifest.json mit dem vollständigen übersetzten Graphen und eine run_results.json mit den Ergebnissen des Laufs, einschließlich Laufzeiten und Zeilenzahlen. dbt docs generate erzeugt eine catalog.json, die jede Spalte jedes Modells beschreibt.
Diese Artefakte sind die Grundlage für die Anbindung an Plattformen für Datenkatalog und Governance. Dawiso liest dbt-Manifeste ein und füllt damit selbsttätig seinen Datenkatalog mit Beschreibungen der Modelle, Definitionen der Spalten, Beziehungen der Lineage und der Abdeckung durch Tests. Aus der Dokumentation, die Ihr Team in schema.yml schreibt, wird so ohne doppelte Arbeit ein durchsuchbarer und geregelter Katalog für das Unternehmen. Über das Katalogisieren hinaus erlauben die Metadaten aus dbt es Dawiso, die Lineage auf Spaltenebene zu zeigen, also welche Quellspalten in welche Mart-Spalten fließen, dazu die Abdeckung der Datenqualität, also welche Modelle Zusicherungen aus Tests tragen, und die Zusagen zur Aktualität, also wann jedes Modell zuletzt erfolgreich gebaut wurde.
In Aktion erleben
Interactive Data Lineage
Sichtbar machen, wie Daten sich bewegen, sich transformieren und über Systeme, Anwendungen und Berichte hinweg zusammenhängen.