Loading...
E.G.

dbt auf ClickHouse: Projekt-Layout und die Fallstricke

29. Juli 2026
Inhaltsverzeichnis

Fast jedes dbt-Tutorial geht davon aus, dass du es auf Snowflake, BigQuery oder Postgres richtest. Ich fahre dbt Core gegen ClickHouse, und das ist ein so anderes Ziel, dass der generische Rat einen still in die Irre führt. Der Adapter ist gut — ClickHouse pflegt ihn selbst — aber das Projekt-Layout, die Materialisierungen und ein paar scharfe Kanten sind ClickHouse-spezifisch. So strukturiere ich es, samt der Fallstricke, die ich mir vorher gewünscht hätte, aufgeschrieben zu sehen. (Wo ClickHouse selbst läuft, steht in ClickHouse auf dediziertem Server; der Gesamt-Stack in der Stack-Beschreibung.)

dbt Core und der ClickHouse-Adapter

Ich nutze dbt Core, nicht Cloud — es ist einfach ein Python-Paket in der Pipeline, versionsgepinnt neben dem dbt-clickhouse-Adapter, den ClickHouse Inc. pflegt. Das zählt: Weil er First-Party ist, versteht er ClickHouses Engines (er materialisiert in ReplicatedMergeTree, treibt Materialized Views, fragt S3 direkt ab), statt ClickHouse wie ein generisches SQL-Warehouse zu behandeln. Der Adapter ist jünger als die für Snowflake oder BigQuery, also stößt du gelegentlich an eine Kante, die die nie sehen — aber für Standard-Modellierung ist er solide.

Projekt-Layout: staging → intermediate → marts

Die Struktur ist der übliche dbt-Dreischichter, mit einer Namenskonvention, die die Rolle jedes Modells aus dem Namen erkennbar macht: stg_<source>_<entity> für Raw-zu-typisiert, int_<domain>_<purpose> für Joins und Anreicherung dazwischen, und mart_<domain>_<entity> für die Tabellen, die die BI-Schicht tatsächlich liest. Staging und Intermediate sind billige Views; nur die Marts werden als echte Tabellen materialisiert.

models:
  my_project:
    staging:
      +materialized: view
      +schema: staging
    intermediate:
      +materialized: view
      +schema: intermediate
    marts:
      +materialized: incremental
      +schema: marts

Layer pro Datenbank, nicht Schema-Suffixe

Hier die erste ClickHouse-spezifische Entscheidung. Viele dbt-Setups halten alles in einer Datenbank und trennen Schichten per Schema-Namens-Suffix. Ich lege jede Schicht in eine eigene ClickHouse-Datenbank — staging, intermediate, marts als drei Datenbanken. Zwei Gründe, beide praktisch: ClickHouse-Grants gelten pro Datenbank, also ist es trivial, dem BI-Tool nur Lesezugriff auf marts zu geben und die rohe Staging-Schicht nie freizulegen; und der Schema-Browser des BI-Tools zeigt drei saubere Namespaces statt einer überladenen Liste. Es kostet nichts und hält die Grenze zwischen Roh und Präsentation echt durchgesetzt.

Materialisierungen: Views billig, Marts incremental

Staging und Intermediate als Views heißt: kein Speicher, immer die aktuelle Quelle — die Arbeit passiert zur Query-Zeit, was okay ist, weil ClickHouse schnell ist und diese Schichten nicht direkt von Nutzern abgefragt werden. Die Marts sind incremental, und hier beißt ClickHouse, wenn du ein generisches dbt-Modell kopierst: Ein Incremental-Modell auf ClickHouse braucht eine Table-Engine und einen Sort-Key, sonst gibt's entweder einen Fehler oder eine Tabelle, die still Duplikate sammelt.

{{ config(
    materialized='incremental',
    engine='MergeTree()',
    order_by='(event_date, entity_id)',
    incremental_strategy='append'
) }}

select *
from {{ ref('int_events_enriched') }}
{% if is_incremental() %}
where event_date > (select max(event_date) from {{ this }})
{% endif %}

Das order_by ist der MergeTree-Sort-Key und entscheidet später über die Query-Geschwindigkeit; nimm die Spalten, nach denen du am häufigsten filterst und gruppierst. Der Incremental-Filter hält jeden Lauf billig, indem er nur Zeilen neuer als der Bestand zieht — ein Datums-Cursor ist die verlässliche Wahl. Kann deine Quelle Historie nachträglich ändern, nutze die Delete+Insert-Strategie auf einem Unique-Key statt Append, oder ein wöchentliches Full-Refresh, damit spät eintreffende Korrekturen nicht hängen bleiben.

Dev vs. Prod ohne Secret-Leck

Zwei Targets in einer profiles.yml, die gitignored ist — committet ist nur eine profiles.yml.example. Dev läuft vom Laptop gegen ClickHouse über einen SSH-Tunnel; Prod läuft vom Orchestrator gegen die interne Adresse. Gleiche Modelle, anderes Target, keine Zugangsdaten im Git.

# dev: SSH-Tunnel  ->  ssh -L 8123:localhost:8123 clickhouse-host
my_project:
  target: dev
  outputs:
    dev:
      type: clickhouse
      host: localhost
      port: 8123
      schema: dev
    prod:
      type: clickhouse
      host: 10.0.0.10   # interne Adresse, vom Orchestrator erreichbar
      port: 8123
      schema: marts

Die Fallstricke, die generische dbt-Tutorials auslassen

Drei Dinge haben mich Zeit gekostet. Erstens Incremental ohne Engine + order_by — oben behandelt, aber die Nummer-eins-Überraschung, wenn man von Snowflake kommt. Zweitens Duplikate bei Re-Runs: ClickHouse erzwingt keine Primary Keys, also dupliziert ein Append-Incremental, das überlappende Zeilen erneut verarbeitet, sie stillschweigend — deine Cursor-Logik ist das Einzige, was schützt, also teste sie. Drittens nicht jedes dbt-Package funktioniert: manche Community-Packages nehmen Postgres- oder Snowflake-Dialekt an und erzeugen still ungültiges ClickHouse-SQL; dbt-utils läuft meist, aber prüfe alles andere gegen einen echten Lauf, statt zu vertrauen, dass es kompiliert.

Häufige Fragen

Ist der dbt-clickhouse-Adapter produktionsreif?

Ja, für Standard-Modellierung — er wird von ClickHouse gepflegt und unterstützt die Engines, Incremental-Strategien und Materialized Views, die man wirklich braucht. Er ist jünger als die Snowflake-/BigQuery-Adapter, also triffst du evtl. gelegentlich eine raue Kante, aber nichts, was ein normales staging/marts-Projekt blockiert.

Warum Views für Staging statt Tabellen?

Views kosten keinen Speicher und spiegeln immer die aktuelle Quelle, und Staging wird nicht direkt von Nutzern abgefragt — es als Tabelle zu materialisieren würde nur Speicher und einen Refresh-Schritt ohne Nutzen bringen. Echte Tabellen (incremental) sind für die Marts reserviert, die wiederholt abgefragt werden.

Wie vermeide ich doppelte Zeilen in Incremental-Modellen?

ClickHouse erzwingt keine Eindeutigkeit, du kannst also nicht auf einen Primary Key zum Deduplizieren bauen. Entweder hältst du den Incremental-Filter streng monoton (ein Datums-/Zeitstempel-Cursor mit sauberem >-Vergleich), nutzt die Delete+Insert-Strategie auf einem Unique-Key, oder läufst periodisch ein Full-Refresh. Teste immer einen Re-Run über überlappende Daten und zähle die Zeilen.

Kann ich dbt Cloud mit ClickHouse nutzen?

Der Adapter arbeitet darunter gleich, aber ich fahre dbt Core, weil es einfach ein Paket ist, das der Orchestrator aufruft — kein Extra-Dienst, keine Per-Seat-Kosten, und das Ganze bleibt self-hosted. Für ein Solo- oder kleines Setup ist Core plus Orchestrator alles, was man braucht.

Verwandte Artikel