Adrienne Vermorel

Définir des métriques dans dbt : bonnes pratiques et patterns

Vous avez configuré vos semantic models dans dbt avec les entities, dimensions et measures. Reste maintenant l’étape qui bloque la plupart des équipes : écrire des métriques qui fonctionnent réellement.

La bonne nouvelle ? La conception de métriques suit des patterns prévisibles. Une fois les cinq types de métriques et quelques principes d’organisation assimilés, vous pouvez exprimer la quasi-totalité de vos calculs métier. Ce tutoriel passe en revue chaque type de métrique avec des exemples concrets, puis aborde les conventions de nommage et les patterns d’organisation qui gardent vos métriques maintenables à mesure que votre projet grandit.

Les cinq types de métriques

MetricFlow prend en charge cinq types de métriques. Chacun a un rôle précis, et choisir le bon détermine si votre calcul est correct.

Simple metrics

Les simple metrics référencent une seule measure avec une agrégation. Ce sont les briques de base de tout le reste.

metrics:
- name: order_total
label: "Total Order Value"
description: "Sum of all order values"
type: simple
type_params:
measure: order_value

C’est tout. La measure order_value définit déjà son agrégation (probablement sum), donc la métrique ne fait que la référencer. Utilisez les simple metrics pour les comptages et sommes de base : revenu total, nombre de commandes, utilisateurs actifs.

Cumulative metrics

Les cumulative metrics agrègent sur des fenêtres temporelles. Pensez aux utilisateurs actifs hebdomadaires, au revenu month-to-date ou aux moyennes glissantes sur 30 jours.

metrics:
- name: weekly_active_users
label: "Weekly Active Users"
description: "Unique users active in the past 7 days"
type: cumulative
type_params:
measure: active_users
window: 7 days

Deux paramètres contrôlent le comportement :

  • window crée une fenêtre glissante (7 jours, 30 jours)
  • grain_to_date repart de zéro aux limites de période (month-to-date, year-to-date)
metrics:
- name: mtd_revenue
label: "Month-to-Date Revenue"
type: cumulative
type_params:
measure: revenue
grain_to_date: month

Un prérequis surprend souvent : les requêtes utilisant des cumulative metrics avec window doivent inclure metric_time comme dimension. Sans dimension temporelle, la fenêtre glissante n’a pas de point d’ancrage.

Derived metrics

Les derived metrics effectuent des calculs à partir d’autres métriques. Elles sont indispensables pour les marges, les taux de croissance et les comparaisons période sur période.

metrics:
- name: gross_profit
label: "Gross Profit"
type: derived
type_params:
expr: revenue - cost_of_goods_sold
metrics:
- name: revenue
- name: cost_of_goods_sold

Le paramètre expr accepte toute expression SQL valide utilisant les noms des métriques référencées. Pour les calculs période sur période, utilisez offset_window avec un alias :

metrics:
- name: revenue_growth_wow
label: "Revenue Growth % W/W"
type: derived
type_params:
expr: (revenue - revenue_last_week) / revenue_last_week * 100
metrics:
- name: revenue
- name: revenue
offset_window: 7 days
alias: revenue_last_week

L’alias permet de référencer la même métrique à différents décalages temporels dans une seule expression.

Ratio metrics

Les ratio metrics divisent un numérateur par un dénominateur. Pourquoi ne pas utiliser une derived metric ? Parce que les ratios posent un piège mathématique : la somme des ratios n’est pas le ratio des sommes.

Si le magasin A a un taux de conversion de 50 % et le magasin B de 25 %, le taux combiné n’est pas 37,5 %. Il dépend du volume de chaque magasin. Les ratio metrics gèrent cela correctement en sommant numérateur et dénominateur séparément avant de diviser.

metrics:
- name: conversion_rate
label: "Conversion Rate"
type: ratio
type_params:
numerator: conversions
denominator: sessions

Vous pouvez appliquer des filtres uniquement au numérateur ou au dénominateur :

metrics:
- name: mobile_conversion_rate
label: "Mobile Conversion Rate"
type: ratio
type_params:
numerator:
name: conversions
filter:
- "{{ Dimension('session__device_type') }} = 'mobile'"
denominator: sessions

Conversion metrics

Les conversion metrics suivent le parcours d’un événement de base vers un événement de conversion dans une fenêtre temporelle. Pensez analyse de funnel : visites vers achats, inscriptions vers activations.

metrics:
- name: visit_to_purchase_rate
label: "Visit to Purchase Rate"
type: conversion
type_params:
entity: user
calculation: conversion_rate
base_measure: visits
conversion_measure: purchases
window: 7 days

Le paramètre entity définit la clé de jointure entre les événements de base et de conversion. Le paramètre calculation peut être conversion_rate (pourcentage) ou conversions (comptage).

Pour un matching plus strict, constant_properties garantit la correspondance des attributs entre les événements :

type_params:
entity: user
calculation: conversion_rate
base_measure: visits
conversion_measure: purchases
window: 7 days
constant_properties:
- base_property: "{{ Dimension('visit__device_type') }}"
conversion_property: "{{ Dimension('purchase__device_type') }}"

Ici, une conversion n’est comptée que si le type d’appareil de l’utilisateur correspond entre la visite et l’achat.

Des conventions de nommage qui passent à l’échelle

Un nommage cohérent rend les métriques faciles à trouver.

Names vs labels

Le champ name est destiné au code. Le champ label est destiné aux humains. Gardez-les distincts :

name: revenue_growth_mom
label: "Revenue Growth % M/M"

Les names utilisent le snake_case, tout en minuscules. Les labels utilisent les majuscules appropriées et peuvent inclure des symboles comme %.

Patterns par type de métrique

Chaque type de métrique bénéficie d’un pattern de nommage différent :

TypePatternExemple
Simple{nom}_{agrégation}order_count, revenue_sum
Cumulative{période}_{métrique}weekly_active_users, mtd_revenue
Derived{métrique}_growth_{période}revenue_growth_mom, orders_growth_yoy
Ratio{numérateur}_per_{dénominateur}revenue_per_customer, orders_per_session
Conversion{action}_to_{action}_ratevisit_to_buy_rate, signup_to_activate_rate

Soyez précis

Des noms vagues créent de la confusion. revenue peut désigner le revenu brut, net ou ajusté. response_time peut être en millisecondes ou en secondes.

Mieux :

  • gross_revenue
  • net_revenue_after_refunds
  • response_time_seconds
  • response_time_p95_ms

Regroupez les métriques liées

Utilisez des préfixes cohérents pour regrouper les métriques apparentées :

# Famille revenue
revenue_total
revenue_per_order
revenue_growth_mom
revenue_mtd
# Famille customer
customer_count
customer_lifetime_value
customer_acquisition_cost
customer_retention_rate

Quand quelqu’un cherche “revenue”, il trouve toutes les métriques liées au revenu au même endroit.

Organiser les métriques dans les grands projets

Les petits projets peuvent définir semantic models et métriques dans le même fichier. Les grands projets ont besoin de plus de structure.

Structure co-localisée

Pour les projets de moins de 20 métriques, gardez tout ensemble :

models/
marts/
mrt__finance__orders.sql
mrt__finance__orders.yml # semantic model + métriques
mrt__sales__customers.sql
mrt__sales__customers.yml # semantic model + métriques

Le fichier YAML contient à la fois la définition du semantic model et les métriques construites à partir de celui-ci.

Structure en sous-dossiers parallèles

Pour les projets plus importants, séparez les semantic models des métriques et organisez par domaine :

models/
marts/
mrt__finance__orders.sql
mrt__sales__customers.sql
semantic_models/
orders.yml
customers.yml
metrics/
revenue_metrics.yml
customer_metrics.yml
conversion_metrics.yml

Cette structure passe à l’échelle parce que les métriques couvrent souvent plusieurs semantic models. Une métrique customer_lifetime_value peut référencer des measures provenant des semantic models orders et customers. Un dossier dédié aux métriques évite les choix de placement arbitraires.

Une seule primary entity par semantic model

Chaque semantic model doit avoir exactement une primary entity. Cette contrainte garde le graphe sémantique navigable.

semantic_models:
- name: orders
defaults:
agg_time_dimension: ordered_at
model: ref('mrt__finance__orders')
entities:
- name: order
type: primary
- name: customer
type: foreign
- name: product
type: foreign

La primary entity (order) identifie ce que chaque ligne représente. Les foreign entities (customer, product) permettent les jointures vers d’autres semantic models.

Patterns de métriques avancés

Les métriques du monde réel nécessitent plus que de simples agrégations.

Comparaisons période sur période

Le paramètre offset_window décale une métrique dans le passé :

metrics:
- name: bookings_vs_last_week
label: "Bookings Change vs Last Week"
type: derived
type_params:
expr: bookings - bookings_7_days_ago
metrics:
- name: bookings
- name: bookings
offset_window: 7 days
alias: bookings_7_days_ago

Pour un changement en pourcentage :

expr: (bookings - bookings_7_days_ago) / NULLIF(bookings_7_days_ago, 0) * 100

Le NULLIF empêche la division par zéro quand la semaine précédente n’avait aucune réservation.

Métriques filtrées

Appliquez des filtres avec le templating Jinja :

metrics:
- name: enterprise_revenue
type: simple
type_params:
measure: revenue
filter:
- "{{ Dimension('customer__segment') }} = 'enterprise'"

Pour les dimensions temporelles avec une granularité spécifique :

filter:
- "{{ TimeDimension('order__ordered_at', 'month') }} >= '2024-01-01'"

Gestion des nulls dans les séries temporelles

Les métriques sans données pour une période retournent null, ce qui crée des trous dans les graphiques. Deux paramètres corrigent cela :

type_params:
measure:
name: revenue
fill_nulls_with: 0
join_to_timespine: true

join_to_timespine: true garantit que chaque date apparaît dans les résultats. fill_nulls_with: 0 remplace les nulls par des zéros. Ensemble, ils produisent des séries temporelles complètes, sans trous.

Tests et validation

MetricFlow valide les configurations à trois niveaux :

  1. Validation du parsing : le YAML respecte-t-il le schéma ?
  2. Validation sémantique : les noms sont-ils uniques ? Les références existent-elles ? Y a-t-il exactement une primary entity ?
  3. Validation de la plateforme : les colonnes référencées existent-elles dans les tables physiques ?

Lancez toutes les validations avec :

Terminal window
# dbt Cloud
dbt sl validate
# dbt Core
mf validate-configs

Ajoutez --verbose-issues --show-all pour un output détaillé lors du débogage.

Intégration CI

Ajoutez la validation à votre pipeline CI pour détecter les changements cassants :

.github/workflows/dbt.yml
- name: Validate semantic layer
run: dbt sl validate

Cela empêche de merger des PRs qui cassent les définitions de métriques.

Anti-patterns à éviter

Modèles ad hoc pour les métriques

Ne créez pas un nouveau modèle dbt juste pour définir une métrique :

-- Mauvais : models/metrics/monthly_revenue.sql
SELECT
DATE_TRUNC('month', ordered_at) AS month,
SUM(amount) AS monthly_revenue
FROM {{ ref('mrt__finance__orders') }}
GROUP BY 1

Cela duplique la logique de transformation. Définissez plutôt la métrique sur votre modèle mart existant et laissez MetricFlow gérer l’agrégation.

Somme de ratios

Ne faites jamais la moyenne de pourcentages ou de taux :

# Faux : donnera des résultats mathématiquement incorrects
- name: avg_conversion_rate
type: simple
type_params:
measure: conversion_rate # C'est déjà un pourcentage par ligne

Utilisez une ratio metric avec des measures séparées pour le numérateur et le dénominateur. Le semantic layer calcule le ratio après avoir agrégé les composants.

Filtres en dur dans les measures

Les measures avec des filtres intégrés réduisent la flexibilité :

# Rigide
measures:
- name: enterprise_revenue
expr: CASE WHEN segment = 'enterprise' THEN amount END
agg: sum

Mieux vaut définir une measure générale et appliquer les filtres au niveau de la métrique :

measures:
- name: revenue
expr: amount
agg: sum
metrics:
- name: enterprise_revenue
type: simple
type_params:
measure: revenue
filter:
- "{{ Dimension('customer__segment') }} = 'enterprise'"

Vous pouvez ensuite créer smb_revenue, startup_revenue ou n’importe quel autre segment sans définir de nouvelles measures.

Descriptions manquantes

Des métriques sans description deviennent des mystères :

# Que mesure-t-on ici ?
- name: arr
type: simple
type_params:
measure: arr_value

Incluez toujours description et label :

- name: arr
label: "Annual Recurring Revenue"
description: "Sum of annualized contract values for active subscriptions, excluding one-time fees"
type: simple
type_params:
measure: arr_value

Dans six mois, quelqu’un vous remerciera.

Et ensuite

Les simple metrics sur vos indicateurs métier principaux (revenu, commandes, utilisateurs) sont le bon point de départ. Faites-les fonctionner dans vos outils BI avant d’ajouter de la complexité.

Ensuite, une seule derived metric comme une comparaison période sur période ou un taux de croissance vous fera pratiquer le pattern offset_window, qui couvre la majorité des besoins de reporting.

Vos modèles dbt existants contiennent probablement de la logique métier qu’il vaut la peine de migrer : filtres en dur, pourcentages calculés et tables pré-agrégées sont autant de candidats pour de véritables métriques dans le semantic layer.

Inutile de définir toutes les métriques possibles dès le départ. Quelques exemples bien structurés donneront à votre équipe des patterns à suivre au fil des nouveaux besoins.