ServicesÀ proposNotesContact Me contacter →
EN FR
Note

Macros introspectives de dbt-utils

Fonctionnement des macros d'introspection à la compilation de dbt-utils — get_column_values, get_relations_by_pattern, get_query_results_as_dict et get_single_value — et quand elles posent problème.

Planté
dbtdata engineeringdata modeling

Les macros introspectives requêtent votre base de données à la compilation pour informer votre SQL. Elles sont plus puissantes que les générateurs SQL statiques — elles permettent à vos modèles de s’adapter à l’état réel de vos données. Mais cette puissance vient avec une contrainte : les relations cibles doivent déjà exister lorsque vous exécutez dbt compile ou dbt run.

C’est le compromis fondamental de toutes les macros introspectives. Planifiez en conséquence.

Comment fonctionne l’introspection à la compilation

Lorsque dbt s’exécute, il passe par deux phases :

  1. Phase de compilation — Les templates Jinja sont évalués, les macros s’exécutent et le SQL est généré. C’est quand les macros introspectives exécutent leurs requêtes de base de données.
  2. Phase d’exécution — Le SQL compilé est envoyé à l’entrepôt et exécuté.

Les macros introspectives exécutent des requêtes de base de données pendant la phase 1, avant que des modèles aient été construits dans l’exécution courante. Cela signifie :

  • Les tables sources sont acceptables (elles existent toujours)
  • Les modèles dbt en amont construits lors d’exécutions précédentes sont acceptables
  • Les modèles dbt en amont qui n’ont pas encore été construits feront échouer la macro

Ce n’est pas un bug — c’est la contrainte intentionnelle de l’introspection à la compilation. Toute macro qui requête la base de données pendant execute plutôt qu’à la compilation est une autre bête (c’est ce que fait run_query() dans le SQL du modèle).

get_column_values

Récupère les valeurs distinctes d’une colonne. Le cas d’usage canonique est d’alimenter une macro pivot avec l’ensemble courant des valeurs de catégorie :

{% set status_values = dbt_utils.get_column_values(
table=ref('base__shopify__orders'),
column='order__status',
order_by='COUNT(*) DESC',
max_records=50,
default=['pending', 'completed', 'cancelled']
) %}

Paramètres à connaître :

  • order_by — contrôle l’ordre des valeurs retournées, qui détermine l’ordre des colonnes dans un pivot
  • max_records — plafonne le nombre de valeurs distinctes retournées
  • default — la liste de repli si la requête échoue (voir ci-dessous)

Fournissez toujours une liste default. La requête d’introspection s’exécute contre l’entrepôt à la compilation. Si le modèle n’existe pas encore — première exécution dans un nouvel environnement, nouveau modèle, CI avec un schéma vierge — la requête échoue et dbt produit une erreur. La liste default empêche cela en donnant à la macro quelque chose à retourner lorsque la relation n’est pas là.

-- Cela cassera à la première exécution dans un environnement vierge
{% set statuses = dbt_utils.get_column_values(
table=ref('base__shopify__orders'),
column='order__status'
) %}
-- C'est sécurisé
{% set statuses = dbt_utils.get_column_values(
table=ref('base__shopify__orders'),
column='order__status',
default=['pending', 'confirmed', 'shipped', 'delivered', 'cancelled']
) %}

La liste default est aussi ce qui s’exécute lorsque dbt parse le projet (la phase parse), puisque le flag execute est false pendant le parsing. La liste default détermine donc quel SQL est compilé pendant dbt parse, et la requête en direct détermine quel SQL est compilé pendant dbt run.

get_filtered_columns_in_relation

Retourne les noms de colonnes d’une relation, avec des exclusions optionnelles. Comme star, c’est utile pour construire des listes de colonnes dynamiques :

{% set columns = dbt_utils.get_filtered_columns_in_relation(
from=ref('base__shopify__orders'),
except=['_fivetran_synced', '_loaded_at']
) %}

Contraintes :

  • Ne fonctionne que sur les relations matérialisées, pas sur les CTEs
  • Retourne une liste vide pendant le mode parse (c’était un bug, corrigé en v1.3.2)

En pratique, star dans la clause SELECT couvre la plupart des cas où vous souhaitez sélectionner toutes les colonnes sauf quelques-unes. get_filtered_columns_in_relation est utile quand vous avez besoin de la liste de colonnes comme variable Jinja pour faire autre chose avec — la passer à une autre macro, générer du SQL personnalisé par colonne, etc.

get_relations_by_pattern

Découvre des relations correspondant à un pattern SQL wildcard. C’est l’approche standard pour consolider des tables date-shardées, courantes avec les exports GA4 de BigQuery et d’autres produits Google :

{% set sharded_tables = dbt_utils.get_relations_by_pattern(
schema_pattern='analytics_%',
table_pattern='events_%',
exclude='%deprecated'
) %}
{{ dbt_utils.union_relations(relations=sharded_tables) }}

Les arguments de pattern utilisent la syntaxe wildcard SQL : % correspond à toute séquence de caractères. Le paramètre exclude s’applique comme second filtre après la correspondance initiale du pattern.

La valeur retournée est une liste d’objets relation, que vous pouvez passer directement à union_relations ou itérer en Jinja.

Quand cette complexité vaut la peine : Lorsque vous avez des noms de tables réellement dynamiques qui croissent dans le temps. Les exports date-shardés BigQuery (events_20240101, events_20240102, …) en sont l’exemple principal — de nouveaux shards apparaissent quotidiennement, et une approche basée sur des patterns les gère automatiquement. Si vous avez un ensemble fixe de tables avec des noms prévisibles, codez-les simplement en dur dans un appel union_relations.

La requête de découverte s’exécute à la compilation, donc les tables doivent exister lors de l’exécution de dbt. C’est généralement acceptable pour les tables shardées car elles sont remplies en externe.

get_query_results_as_dict et get_single_value

Ces macros exécutent du SQL arbitraire et retournent les résultats sous forme de structures de données Jinja.

get_single_value (ajouté en v1.0) retourne une seule valeur scalaire. Utile pour les seuils dynamiques :

{% set max_date = dbt_utils.get_single_value(
"SELECT MAX(order__created_at) FROM " ~ ref('base__shopify__orders')
) %}

get_query_results_as_dict exécute du SQL arbitraire et retourne un dictionnaire. La requête doit retourner exactement deux colonnes : la première devient la clé, la seconde la valeur.

{% set exchange_rates = dbt_utils.get_query_results_as_dict(
"SELECT currency__code, exchange__rate FROM " ~ ref('base__finance__exchange_rates')
) %}

Les deux macros exécutent leurs requêtes à la compilation. La même contrainte de première exécution s’applique : les relations référencées doivent déjà exister.

Utilisez ces macros avec parcimonie. Chaque macro introspective ajoute une requête à votre étape de compilation. Un projet avec dix macros introspectives exécute dix requêtes avant que des modèles ne soient construits. Sur un entrepôt lent ou avec des connexions froides, cela ralentit matériellement dbt compile et dbt run. Le nombre de requêtes rend aussi les pipelines CI plus difficiles à comprendre.

Le coût de performance vaut la peine d’être payé pour la découverte dynamique de schémas (get_relations_by_pattern) et les configurations de pivot (get_column_values). Il ne vaut généralement pas la peine d’être payé pour des choses que vous pourriez simplement coder en dur ou calculer dans le SQL propre du modèle.

Le garde execute

Si vous écrivez des macros introspectives personnalisées qui appellent run_query(), incluez toujours le garde execute :

{% macro my_introspective_macro(table, column) %}
{% if not execute %}
{{ return([]) }}
{% endif %}
{% set query %}
SELECT DISTINCT {{ column }} FROM {{ table }}
{% endset %}
{% set results = run_query(query) %}
{{ return(results.columns[0].values()) }}
{% endmacro %}

Les macros dbt-utils gèrent cela en interne. Mais si vous les étendez ou les enveloppez, vous devez comprendre pourquoi le garde existe : pendant la phase de parse de dbt, execute est False. Tout appel à run_query() sans ce garde tentera de s’exécuter contre la base de données pendant le parsing, lorsque les références de tables peuvent ne pas encore être entièrement résolues. Le garde fait retourner à la macro une valeur vide sécurisée pendant le parse et exécute la vraie requête pendant l’exécution.

Ce pattern est couvert plus en profondeur dans Macros dbt.