ServicesÀ proposNotesContact Me contacter →
EN FR
Note

Anatomie d'une matérialisation dbt

La structure en six étapes que suit toute matérialisation dbt — setup, pre-hooks, SQL principal, post-hooks, cleanup et return — ainsi que les objets clés et les méthodes d'adapter.

Planté
dbtdata engineeringdata modeling

Une matérialisation personnalisée est une macro avec une structure spécifique. Les matérialisations intégrées de dbt — table, view, incremental et ephemeral — suivent toutes le même squelette en six étapes.

La Structure en Six Étapes

Chaque matérialisation — intégrée ou personnalisée — suit ce modèle :

{%- materialization my_custom_mat, adapter='default' -%}
{# 1. SETUP - Préparer les relations #}
{% set target_relation = this.incorporate(type='table') %}
{% set existing_relation = load_cached_relation(this) %}
{# 2. PRE-HOOKS #}
{{ run_hooks(pre_hooks) }}
{# 3. SQL PRINCIPAL - Construire la relation #}
{% call statement('main') %}
{{ sql }}
{% endcall %}
{# 4. POST-HOOKS #}
{{ run_hooks(post_hooks) }}
{# 5. CLEANUP - Grants, docs, permissions #}
{% do apply_grants(target_relation, grant_config, should_revoke) %}
{% do persist_docs(target_relation, model) %}
{# 6. COMMIT ET RETURN #}
{% do adapter.commit() %}
{{ return({'relations': [target_relation]}) }}
{%- endmaterialization -%}

Le bloc materialization remplace le bloc macro que vous utiliseriez pour une macro ordinaire. Le second argument — adapter='default' ou adapter='bigquery' — scoped la matérialisation à un adapter spécifique. Utilisez default quand votre logique est agnostique au data warehouse, ou spécifiez un adapter quand elle repose sur du DDL propre à l’entrepôt.

La structure est rigide pour une raison. Le runtime de dbt attend que certaines choses se produisent à certains moments. Les pre-hooks s’exécutent avant le SQL principal. Les post-hooks s’exécutent après. Les grants et la documentation sont appliqués lors du cleanup. Si vous sautez des étapes ou les réordonnez, vous obtenez des échecs inattendus ou des permissions silencieusement manquantes.

Objets de Contexte Clés

Quatre objets sont disponibles dans chaque matérialisation :

this est la relation cible — le chemin complet database.schema.model_name où le modèle sera matérialisé. Utilisez this.incorporate(type='table') pour définir explicitement le type de relation. C’est important parce que le même nom de modèle a peut-être été précédemment matérialisé en tant que vue, et vous devez gérer cette incompatibilité.

sql contient l’instruction SELECT compilée du fichier de modèle. C’est le SQL de l’utilisateur une fois tout le rendu Jinja effectué — refs résolues, macros développées, is_incremental() évalué. Votre matérialisation enveloppe ce SQL dans le DDL approprié (CREATE TABLE AS, INSERT INTO, MERGE, etc.).

config contient la configuration du modèle — tout ce que l’utilisateur a passé dans le bloc config(). Accédez aux valeurs avec config.get('my_setting', default_value). C’est ainsi que vous rendez les matérialisations configurables : l’utilisateur définit le comportement dans son modèle, et la matérialisation le lit.

adapter fournit des méthodes spécifiques à la base de données pour manipuler les relations. C’est votre interface principale pour les opérations DDL.

Méthodes d’Adapter Utilisées en Permanence

L’objet adapter et quelques fonctions helper gèrent la plupart de ce dont une matérialisation a besoin :

{# Vérifier si la relation existe déjà dans le cache de dbt #}
{% set existing = load_cached_relation(this) %}
{# Obtenir les informations de colonnes d'une relation #}
{% set columns = adapter.get_columns_in_relation(target_relation) %}
{# Supprimer une relation #}
{% do adapter.drop_relation(old_relation) %}
{# Renommer une relation #}
{% do adapter.rename_relation(temp_relation, target_relation) %}
{# Créer un nom de relation temporaire basé sur la cible #}
{% set temp_relation = make_temp_relation(target_relation) %}
{# Créer un nom de relation de sauvegarde basé sur la cible #}
{% set backup_relation = make_backup_relation(target_relation) %}
{# Valider la transaction #}
{% do adapter.commit() %}

load_cached_relation() retourne la relation si elle existe dans le cache de métadonnées de dbt, ou none si elle n’existe pas. C’est ainsi que vous détectez la première exécution versus les exécutions suivantes — une distinction critique car votre matérialisation doit gérer les deux cas. Lors de la première exécution, il n’y a rien à supprimer, renommer ou valider. Lors des exécutions suivantes, vous devez décider quoi faire avec la table existante.

Le gestionnaire de contexte statement() exécute le SQL et l’enregistre dans le système de journalisation de dbt. Utilisez des instructions nommées comme statement('main') pour la construction principale et statement('validate', fetch_result=True) quand vous avez besoin de récupérer les résultats de requête. Le flag fetch_result=True stocke le résultat pour y accéder plus tard avec load_result('validate').

{% call statement('validate', fetch_result=True) %}
SELECT COUNT(*) AS row_count FROM {{ temp_relation }}
{% endcall %}
{% set row_count = load_result('validate')['data'][0][0] %}

Ce pattern — exécuter une requête, récupérer le résultat, l’utiliser pour une logique conditionnelle — est la façon dont les matérialisations implémentent des étapes de validation, des comptages de lignes, des comparaisons de schémas, ou toute logique qui dépend de l’état des données.

Gérer les Incompatibilités de Type

Un pattern que vous répéterez dans presque chaque matérialisation personnalisée : vérifier si la relation existante correspond au type que vous attendez. Si un modèle a été précédemment matérialisé en tant que vue et que vous le passez à une matérialisation de table personnalisée, la relation existante est une vue, pas une table. Essayer de la renommer ou de la permuter comme si c’était une table échouera.

{% if existing_relation is not none and existing_relation.type != 'table' %}
{% do adapter.drop_relation(existing_relation) %}
{% set existing_relation = none %}
{% endif %}

Supprimez la relation incompatible et traitez la situation comme une première exécution. C’est du code défensif qui prévient une catégorie d’erreurs qui n’apparaissent autrement que lorsque quelqu’un change le type de matérialisation d’un modèle dans un projet qui tourne depuis des mois.

Où Vivent les Matérialisations

Placez les fichiers de matérialisation dans macros/materializations/ de votre projet dbt. Le nom du fichier doit correspondre au nom de la matérialisation — zero_downtime_table.sql pour materialization zero_downtime_table. dbt les découvre automatiquement ; aucune étape d’enregistrement n’est nécessaire.

Vous pouvez limiter une matérialisation à un adapter spécifique en définissant adapter='bigquery' (ou tout autre nom d’adapter). Si vous voulez une implémentation par défaut qui fonctionne sur tous les adapters avec des variantes spécifiques, utilisez le pattern dispatch : créez une implémentation default et des variantes spécifiques à chaque adapter que dbt sélectionne automatiquement à l’exécution.

Lien avec les Macros

Les matérialisations sont techniquement des macros — elles utilisent Jinja, elles vivent dans des fichiers .sql, et elles accèdent aux mêmes objets de contexte. La différence réside dans la déclaration du bloc materialization et le contrat implicite avec le runtime de dbt. Une macro génère des fragments SQL qui sont assemblés dans un modèle. Une matérialisation contrôle l’intégralité du cycle de vie de la construction d’un modèle, depuis la vérification de l’état existant jusqu’à la création de la relation finale.

Les mêmes bonnes pratiques des macros s’appliquent : gardez la logique lisible, utilisez {{ log() }} pour le débogage, et vérifiez target/compiled/ pour voir le SQL que votre matérialisation a réellement généré. La sortie compilée est votre meilleur outil de débogage car elle montre exactement ce que dbt a envoyé à l’entrepôt.