Skip to main content
Ce guide présente les aspects spécifiques à ClickHouse de dbt à travers le projet Jaffle Shop for ClickHouse, le portage ClickHouse du projet d’exemple classique de dbt Labs. En partant d’un projet qui se construit déjà, il montre comment :
  1. Comprendre comment les vues et les tables du projet se matérialisent dans ClickHouse.
  2. Charger des données avec des seeds et contrôler les types ClickHouse ainsi que la disposition des tables.
  3. Configurer un model de type table avec un engine ClickHouse, une sorting key et un partitioning.
  4. Transformer une table en model incremental et choisir une incremental strategy.
  5. Créer un snapshot.
  6. Utiliser les materialized views de ClickHouse.
Il est conçu pour être lu en complément du reste de la documentation, de la page features and configurations et de la référence des materializations.

Avant de commencer

Suivez d’abord le README de ClickHouse/jaffle-shop-clickhouse. Il explique comment configurer le projet avec dbt Core 1.x, dbt OSS, dbt v2 ou la plateforme dbt, comment le faire pointer vers un ClickHouse local (docker) ou ClickHouse Cloud, comment charger les données d’exemple avec dbt seed, et comment lancer le premier dbt build. Une fois que dbt build s’est exécuté avec succès, revenez ici pour les exemples et configurations spécifiques à ClickHouse. Après les étapes du README, vous devriez avoir deux bases de données dans ClickHouse :
  • raw : les six tables sources chargées depuis des CSV files par dbt seed (raw_customers, raw_orders, raw_items, raw_products, raw_stores, raw_supplies).
  • jaffle_shop (le schema de votre profile) : six vues de staging (stg_*) et sept tables de mart (customers, orders, order_items, products, locations, supplies, metricflow_time_spine).
Si votre profile utilise un schema différent, remplacez jaffle_shop par votre valeur dans les requêtes ci-dessous.
dbt Core 1.x, dbt OSS, dbt v2 et la plateforme dbt. Toutes les commandes et tous les modèles de ce guide sont identiques dans tous les cas. Les exemples ont été testés avec dbt Core 1.12 et dbt-clickhouse 1.10, ainsi qu’avec dbt OSS 2.0 sur ClickHouse 26.8 ; dbt v2 s’appuie sur le même adapter, et la plateforme dbt s’appuie sur dbt v2. La sortie de console présentée provient de dbt Core 1.x, et les quelques cas où les moteurs se comportent différemment sont signalés. Consultez la page dbt OSS, dbt v2 et plateforme dbt pour connaître l’état actuel de l’adapter v2, et Connect ClickHouse dans la documentation dbt pour faire vos premier pas sur la plateforme dbt.
Tous les SQL statements qui ne sont pas des commandes dbt sont destinés à être exécutés directement sur ClickHouse, par exemple avec clickhouse client, la SQL console de ClickHouse Cloud ou le client SQL de votre choix.

Comment le projet est matérialisé

Jaffle Shop configure ses materializations dans dbt_project.yml : les modèles de staging sont des vues et les marts sont des tables.
Un model view est reconstruit via une instruction CREATE OR REPLACE VIEW à chaque exécution. Il ne stocke aucune donnée, sa construction ne coûte donc rien, mais chaque requête portant sur ce model exécute le SQL du model sur les source tables. ClickHouse conserve le SQL compilé du model dans la view definition :
Un model table est reconstruit de zéro à chaque exécution : l’adapter crée une nouvelle table, exécute un INSERT INTO ... SELECT avec le SQL du model, puis l’échange atomiquement avec la version précédente. Les performances des requêtes sont bien meilleures que celles d’une view, au prix du stockage et d’une reconstruction intégrale de la table à chaque fois. Examinez la table que dbt a créée pour le mart orders :
Deux éléments sont ici propres à ClickHouse. Le model ne déclare pas de table engine : l’adapter utilise donc MergeTree. Il ne déclare pas non plus de sorting key : l’adapter utilise alors ORDER BY tuple(), ce qui signifie que les données ne sont pas triées du tout. Cela convient pour un projet d’exemple, mais pour une table réelle, il vous faudra définir les deux, ce que font les sections suivantes. La page materializations répertorie toutes les configurations de table prises en charge par l’adapter.

Chargement des données avec les seeds

Le projet Jaffle Shop utilise les seeds dbt pour charger ses données brutes depuis les fichiers CSV du répertoire seeds/jaffle-data. Les seeds sont conçus pour de petits jeux de données de référence statiques (tables de codes, correspondances), et non pour alimenter un warehouse ; le projet y recourt par commodité, afin que vous puissiez démarrer sans outil d’ingestion supplémentaire — d’où le fait que les seeds soient désactivés, sauf si vous passez --vars '{"load_source_data": true}'. Les seeds n’en restent pas moins un bon moyen de comprendre comment dbt crée les tables ClickHouse. dbt infère un type de colonne pour chaque colonne du CSV, et les types inférés diffèrent selon les engines : Lorsque le type importe, fixez-le avec column_types. Le projet le fait déjà pour la colonne opened_at du seed raw_stores dans dbt_project.yml :
Les seeds acceptent également les configurations de table ClickHouse engine, order_by et partition_by. Par exemple, pour trier le seed raw_orders par date de commande et le partitionner par mois, ajoutez un fichier de propriétés à côté des fichiers CSV : seeds/jaffle-data/_raw_orders.yml :
Utilisez un fichier de propriétés pour ces configurations de seed ClickHouse plutôt que les clés +order_by ou +engine sous seeds: dans dbt_project.yml. dbt Core 1.x accepte les deux formes, mais dbt v2 ne les reconnaît que dans un fichier de propriétés et rejette les clés de dbt_project.yml avec l’erreur Unrecognized key ... Custom keys must go under +meta.
Rechargez ce seed et examinez la table ainsi produite :
dbt seed --full-refresh supprime et recrée la table ; exécutez donc cette commande avant de construire quoi que ce soit qui dépende directement des données de cette table, comme la vue matérialisée présentée plus loin dans ce guide.

Configurer une table pour ClickHouse

Le mart orders est le point de départ naturel : il est interrogé par le mart customers ainsi que par les metrics du projet, et il s’agit d’une table de type événementiel dotée d’un timestamp. Ajoutez un bloc config en haut de models/marts/orders.sql pour choisir l’engine, la sorting key et un schéma de partitioning :
Le reste du model reste inchangé. materialized='table' reprend ce que dbt_project.yml définit déjà pour les marts, ce qui rend le model self-describing lorsque vous le basculerez plus tard en incremental. Reconstruisez uniquement ce model :
La table possède désormais une sorting key appropriée et une partition par mois :
Outre engine, order_by et partition_by, les modèles de table acceptent primary_key, ttl, settings, query_settings, projections et indexes, et les colonnes peuvent se voir attribuer codec et ttl via un contrat de modèle. Tous ces éléments sont décrits sur la page des matérialisations.

Créer un modèle incrémental

Reconstruire orders intégralement à chaque exécution ne pose pas de problème pour 62 000 lignes, mais devient inadapté pour une table qui grossit de plusieurs millions de lignes par jour. La materialization incrémentale de dbt ne traite que les lignes modifiées depuis la dernière exécution. Convertir le modèle orders nécessite deux ajouts :
  1. unique_key : la colonne qui identifie une ligne, ici order_id. L’adapter s’en sert pour remplacer les lignes traitées à nouveau au lieu de les dupliquer.
  2. Un filtre incrémental : une clause where encadrée par {% if is_incremental() %} qui sélectionne uniquement les lignes à traiter. Elle s’applique lors des exécutions incrémentales, mais pas lors de la première construction de la table (ni lors d’une reconstruction avec --full-refresh). Les commandes portent un timestamp, si bien que le filtre compare ordered_at à la valeur la plus récente déjà présente dans la table, référencée via la variable {{ this }}.
Modifiez models/marts/orders.sql afin que le bloc config et la fin du modèle ressemblent à ceci :
stg_orders tronque ordered_at au jour, le filter utilise donc >= : à chaque exécution, la journée la plus récente est intégralement retraitée et, grâce à unique_key, les rows déjà chargées sont replaced plutôt que dupliquées. C’est ce qui rend l’opération sûre pour les commandes qui arrivent plus tard dans la même journée. Exécutez le model. La table existe déjà : cette première exécution est donc déjà une exécution incremental, seule la journée la plus récente est retraitée.
Ajoutons maintenant de nouvelles données. Les données de Jaffle Shop s’arrêtent en août 2025 ; nous ajoutons donc un nouveau client, Clicky McClickHouse, qui a commandé un jaffle hier. Insérez un client, une commande et sa ligne de commande dans les tables brutes :
L’identifiant du magasin correspond à Philadelphie, l’article est un jaffle nutellaphone who dis? à 11,00 et la taxe correspond aux 6 % de Philadelphie : les tests de données du projet continuent donc de passer. Exécutez l’ensemble du projet afin que les vues de staging et la table order_items voient les nouvelles rows avant orders :
La nouvelle commande figure dans la table incremental et le mart customers, reconstruit à partir de celle-ci, connaît le nouveau client :

Internals

Le query log de ClickHouse montre les statements exécutés par l’adapter pour la mise à jour incrémentale :
La stratégie incrémentale par défaut de l’adapter fonctionne comme suit. Dans les schémas de cette section, une flèche allant d’une table vers un statement signifie que le statement lit cette table ; une flèche allant d’un statement vers une table signifie qu’il y écrit, la mute, la renomme ou la supprime :
  1. Une table orders__dbt_new_data est créée et le SQL du model, y compris le filter incrémental, y est inséré. Lors de l’exécution ci-dessus, 378 rows ont été écrites : les 377 commandes du dernier jour déjà chargées, plus la nouvelle.
  2. Une table orders__dbt_tmp est créée avec la même structure que orders, puis toutes les rows de orders dont l’order_id n’est pas présent dans orders__dbt_new_data y sont copiées.
  3. Toutes les rows de orders__dbt_new_data sont insérées dans orders__dbt_tmp. Ce sont les étapes 2 et 3 qui remplacent les rows du dernier jour au lieu de les dupliquer.
  4. orders__dbt_new_data est supprimée.
  5. orders__dbt_tmp est échangée avec orders au moyen d’un statement atomic EXCHANGE TABLES (via un renommage intermédiaire en orders__dbt_backup), de sorte que orders contient désormais la nouvelle version.
  6. L’ancienne version est supprimée.
L’étape 2 copie la table entière : cette stratégie est donc aussi coûteuse qu’une reconstruction complète de la table sur des modèles très volumineux ; voir les limitations. Les stratégies ci-dessous évitent cette copie.

Append strategy

La stratégie append insère les rows sélectionnées par le model directement dans la target table. Aucune temporary table n’est créée et rien n’est copié : c’est donc l’exécution incremental la moins coûteuse possible. En contrepartie, rien n’est dédupliqué : si le filter incremental sélectionne une row déjà présente dans la table, celle-ci se retrouve en double. Réservez cette stratégie à des données immuables, de type événement, et veillez à ce que le filter ne sélectionne que des rows réellement nouvelles. Avec ordered_at tronqué au jour, cela revient à remplacer le filter par >. Modifiez le model :
Ajoutez un deuxième nouveau client, Danny DeBito, avec une commande passée aujourd’hui à Brooklyn (4 % de taxe) contenant un jaffle et un café :
Le modèle incremental s’est exécuté en une fraction du temps de l’exécution précédente. Les deux nouveaux clients ont exactement une commande dans la table :
Le query log confirme la différence : cette fois, le seul statement portant sur orders est un unique INSERT INTO jaffle_shop.orders ... SELECT ... contenant le SQL du model et le filter incrémental, et il a écrit une seule row.
Avec > et un timestamp tronqué au jour, une commande qui arrive plus tard dans la même journée que la dernière commande chargée ne sera jamais prise en compte. Dans un projet réel, filtrez sur un timestamp en pleine précision, ou sur un temps d’ingestion croissant de façon monotone, lorsque vous utilisez la strategy append.

Stratégie delete et insert

Historiquement, ClickHouse n’a offert qu’une prise en charge limitée des mises à jour et des suppressions, sous la forme de mutations asynchrones. Celles-ci peuvent s’avérer extrêmement coûteuses en IO et sont généralement à éviter. ClickHouse 22.8 a introduit les suppressions légères et ClickHouse 25.7 les mises à jour légères. Avec elles, l’effet d’une instruction de suppression ou de mise à jour est immédiatement visible du point de vue de l’utilisateur, même si elle est matérialisée de manière asynchrone. La stratégie delete+insert repose sur les suppressions légères et se configure via le paramètre incremental_strategy :
Cette stratégie opère directement sur la target table : si une étape échoue à mi-parcours, les données du modèle incremental risquent de se retrouver dans un état invalid, car il n’y a pas de swap atomic. En résumé :
  1. Une temporary table (orders__dbt_new_data_<run_id>) est créée, et les rows sélectionnées par le model y sont insérées.
  2. Un DELETE est exécuté sur orders pour chaque order_id présent dans la temporary table.
  3. Les rows de la temporary table sont insérées dans orders.
  4. La temporary table est supprimée.

Stratégie insert overwrite (expérimentale)

La stratégie insert_overwrite remplace des partitions entières : elle nécessite donc une configuration partition_by, comme la configuration mensuelle définie sur orders. Elle procède selon les étapes suivantes :
  1. Créer une staging table (orders__dbt_new_data_<run_id>) ayant la même structure que orders.
  2. Insérer dans la staging table uniquement les rows sélectionnées par le model.
  3. Lister les partitions présentes dans la staging table à partir de system.parts.
  4. Remplacer exactement ces partitions dans orders par ALTER TABLE ... REPLACE PARTITION ... FROM depuis la staging table.
  5. Supprimer la staging table.
Cette approche présente les avantages suivants :
  • Elle est plus rapide que la stratégie par défaut, car elle ne copie pas l’intégralité de la table.
  • Elle est plus sûre que les autres stratégies, car elle ne modifie pas la table d’origine tant que l’opération INSERT ne s’est pas exécutée avec succès : en cas d’échec intermédiaire, la table d’origine reste inchangée.
  • Elle applique la bonne pratique d’ingénierie des données dite d‘“immutabilité des partitions”, qui simplifie le traitement incrémental et parallèle des données, les rollbacks, etc.
La page des materializations décrit les autres options de la materialization incremental, notamment la strategy microbatch et on_schema_change.

Créer un snapshot

Les snapshots dbt enregistrent l’évolution des rows d’une table mutable au fil du temps, ce qui permet aux analystes de consulter l’état des données à n’importe quel moment du passé. Ils mettent en œuvre les dimensions à évolution lente de type 2 : chaque version d’une row est stockée avec l’interval durant lequel elle était valide. Le mart customers est un bon candidat : count_lifetime_orders, lifetime_spend et customer_type changent tous dès qu’un client passe une nouvelle commande. Avant de poursuivre, rétablissez sur le modèle orders la stratégie incrémentale par défaut de la section incrémentale (supprimez incremental_strategy='append' et remettez le filtre à >=), afin que les commandes passées plus tard dans la journée soient bien prises en compte. Depuis dbt 1.9, les snapshots sont définis en YAML. Créez snapshots/customers_snapshot.yml :
La stratégie check compare les colonnes listées entre le snapshot courant et la source à chaque exécution et enregistre une nouvelle version dès que l’une d’elles change. Si votre modèle dispose d’une colonne timestamp fiable de type “dernière mise à jour”, la stratégie timestamp est moins coûteuse : définissez strategy: timestamp et updated_at: <column>. Le champ last_ordered_at de Jaffle Shop est tronqué au jour : il ne détecterait donc pas une seconde commande passée le même jour, d’où l’utilisation de check dans cet exemple. Prenez le premier snapshot :
La table du snapshot est créée à côté des modèles. La macro generate_schema_name du projet place chaque relation dans le schéma du target pour les targets hors production ; une config schema sur le snapshot ne prendrait donc effet qu’avec le target prod. Elle contient une row par client, avec les columns de suivi dbt dbt_valid_from et dbt_valid_to ; cette dernière vaut NULL pour la version courante d’une row :
Clicky revient prendre un café aujourd’hui :
Exécutez les modèles pour que orders et customers reflètent la nouvelle commande, puis créez un second snapshot :
Clicky compte désormais deux rows dans le snapshot. La première version a été clôturée en renseignant son dbt_valid_to, tandis que la nouvelle version, correspondant à un client returning avec deux commandes, reste ouverte. Danny n’a pas changé : sa row demeure donc intacte :
En coulisses, l’adapter construit la nouvelle version du snapshot dans une table customers_snapshot__snapshot_upsert, puis la met en place avec EXCHANGE TABLES (ou via un drop suivi d’un rename lorsque le serveur ne prend pas en charge l’échange de tables) : les lecteurs voient ainsi soit la version précédente, soit la nouvelle version du snapshot. Consultez la section snapshot de la page materializations pour la référence de configuration.

Utiliser les vues matérialisées

Jusqu’ici, tout nécessite un dbt run pour intégrer de nouvelles données dans les modèles. Les vues matérialisées de ClickHouse fonctionnent différemment : ce sont des déclencheurs à l’insertion. Chaque bloc de lignes inséré dans la table source est transformé par le SELECT de la vue, puis écrit dans une table cible, sans aucune planification. L’adaptateur les expose via la matérialisation materialized_view. Créez models/marts/daily_store_revenue.sql avec le nombre de commandes et le chiffre d’affaires par magasin et par jour, en lisant directement dans la table des commandes brutes :
Les paramètres engine et order_by s’appliquent à la table cible. SummingMergeTree additionne les colonnes numériques des lignes partageant la même clé de tri lorsqu’il fusionne les parts, ce qui correspond exactement au besoin d’une agrégation par jour et par magasin.
L’adapter a créé deux objets : la target table, nommée d’après le model, et la materialized view elle-même, avec le suffix _mv, qui pointe vers la target table via une clause TO. Par défaut (catchup=True), la target table a également été backfillée avec les commandes existantes :
Insérez maintenant une autre commande brute pour Danny, sans exécuter dbt ensuite :
La table cible le reflète déjà. Brooklyn compte désormais deux commandes aujourd’hui :
La requête agrège volontairement avec sum() et GROUP BY : SummingMergeTree ne regroupe les lignes ayant la même clé qu’au moment de la fusion des parts en arrière-plan ; jusque-là, les deux commandes de Brooklyn restent deux lignes distinctes dans la table. Agrégez toujours à la lecture (ou utilisez FINAL) avec les engines de type summing et aggregating. De son côté, le modèle incremental orders ne contient encore qu’une seule commande pour Danny jusqu’au prochain dbt run. Les exécutions suivantes de dbt run conservent la target table et ses données et se contentent de mettre à jour la view definition, via ALTER TABLE ... MODIFY QUERY lorsque la modification le permet : on peut donc sans risque conserver le modèle dans le projet. dbt run --full-refresh reconstruit la target table et en refait le backfill (sauf si catchup vaut False). La page sur les materialized views couvre le reste : les changements de schéma avec on_schema_change, la désactivation du backfill avec catchup, les refreshable materialized views, plusieurs views alimentant la même target et la définition de la target table comme modèle à part entière.

Pour aller plus loin

Ce guide ne fait qu’effleurer dbt. La documentation dbt fait référence pour tout ce qui n’est pas spécifique à ClickHouse. Concernant l’adaptateur, consultez la page features and configurations pour les paramètres de profil et les fonctionnalités globales, la page materializations pour chacune des configurations utilisées ci-dessus, et la page dbt OSS, dbt v2 et plateforme dbt si vous utilisez dbt OSS, dbt v2 ou la plateforme dbt. Les contributions de nouveaux exemples au projet Jaffle Shop for ClickHouse sont les bienvenues.
Dernière modification le 26 septembre 2026