Aller au contenu
dbtFinOpsTestData

Comment maîtriser les coûts cachés des tests dbt sur BigQuery ?

Dans l’ingénierie des données, dbt est un outil clé pour transformer et tester les données. Il automatise l'exécution des tests et la génération des requêtes SQL, garantissant leur qualité avec peu d’efforts. Mais un point reste souvent ignoré : les coûts cachés des tests, parfois très élevés.

Data Build Tool

Sur des plateformes comme BigQuery ou Snowflake, où la facturation repose sur l’usage (volume de données scannées, temps de calcul, etc.), ces tests peuvent représenter une part significative des dépenses, parfois même supérieure au coût de construction des modèles.

Cet article explore comment identifier ces coûts et propose une solution simple pour les rendre visibles directement dans le terminal grâce à une macro personnalisée.

Un coût souvent sous-estimé

Si dbt offre une visibilité sur les coûts de construction des modèles et du traitement incrémental, il ne fournit aucune information sur le coût des tests. Cette absence de transparence peut avoir des conséquences financières importantes, en particulier sur BigQuery, où la facturation en mode on-demand dépend des données scannées.

Prenons un exemple :
Un simple test d’unicité sur une colonne implique généralement la lecture complète de cette colonne, quelle que soit la taille de la table. Sur une table de plusieurs téraoctets, un tel test peut entraîner des coûts prohibitifs.

Le problème vient aussi de la facilité avec laquelle on peut ajouter des tests dans dbt. Une simple ligne dans un fichier yml déclenche une requête SQL qui, par défaut, scanne l’intégralité du champ concerné. Plus le volume de données est grand, plus la facture s’alourdit.

Les tests dans dbt : chaque requête a un coût

Chaque test ajouté génère une requête SQL qui, par défaut, traite l'ensemble du champ testé. Avec une meilleure visibilité sur ces coûts, les équipes peuvent éviter une augmentation inattendue de leurs factures d'entrepôt de données.

version: 2
models:
  - name: my_model
    description: A big big table
    columns:
      - name: transaction_date
        data_tests:
          - not_null
      - name: store_id
      - name: price
      - name: transaction_id
        data_tests:
          - unique
          - not_null

Lorsque les coûts de traitement sont affichés pendant les exécutions de modèle, ils ne reflètent que les coûts de matérialisation du modèle.

Par exemple, si vous construisez le modèle suivant, dbt exécute le modèle "my_model" ainsi que les trois tests définis dans le fichier .yml correspondant. La sortie montre si les tests ont "PASSÉ" ou "ÉCHOUÉ", mais aucune information relative aux coûts n'est fournie.

Cependant, bien que dbt n'affiche pas les coûts des tests, l'entrepôt de données (du moins BigQuery) renvoie ces informations. Ces détails sont stockés dans le fichier target/run_results.json après l'exécution.

Explorer le fichier run_results.json

Nous y voilà :

Toutes les informations retournées par BigQuery sur les tests sont stockées dans ce fichier. Par exemple :

{
  [...]
},
  "results": [
    {
      "status": "pass",
      "timing": [
        {
          "name": "execute",
          "started_at": "2023-12-02T15:46:02.315630Z",
          "completed_at": "2023-12-02T15:46:04.131250Z"
        }
      ],
      "execution_time": 1.8415067195892334,
      "adapter_response": {
        "_message": "SELECT (1.0 rows, 19.9 MiB processed)",
        "code": "SELECT",
        "rows_affected": 1,
        "bytes_processed": 20826352,
        "bytes_billed": 20971520,
        "slot_ms": 8436
      },
      "message": null,
      "failures": 0,
      "unique_id": "test.******.dbt_utils_unique_my_model__transaction_id.3cfdhsyd7ac",
      "compiled_code": "with validation_errors as (select transaction_id from `database`.`dataset`.`my_model` group by transaction_id having count(*) > 1) select * from validation_errors",
      "relation_name": null
    }
  ]
}

Vous pouvez y identifier le nom du test, la table testée, et surtout : les octets facturés.

Une approche simple consisterait à analyser ce fichier avec une macro. Cependant, ce fichier n'est accessible qu'après l'exécution de dbt, car il est écrit à la fin de l'exécution. Heureusement, une version en mémoire de ce fichier est progressivement construite et accessible pendant l'exécution via la variable dbt results.

Une macro custom pour un retour immédiat sur les coûts

Pour remédier à cela, nous pouvons écrire une macro personnalisée qui analyse la variable results pour récupérer les informations de coûts des tests et les afficher directement dans le terminal à la fin de chaque exécution dbt.

Cette macro permet d'afficher les coûts des tests dépassant 0,50$, puis de fournir un résumé du coût total.

{% macro dbt_test_costs_summary(results) %}

  {% set cost_per_TiB = 5 %}
  {% set TiB = 1024**4 %}
  {% set GiB = 1024**3 %}
  {% set MiB = 1024**2 %}
  {% set excluded_run_status = ['error', 'skipped'] %}
  {# Counter initialization #}
  {% set total_billed_data_counter = namespace(tc = 0) %}

  {% if execute %}
    {{ log("========== Expensive tests costs summary ==========", info=True) }}
    {% for res in results if res.node.resource_type == 'test' and res.status not in excluded_run_status -%}
      {# Extract test name #}
      {% set test_name = res.node.unique_id.split('.')[2] %}
      {# Extract tested table name #}
      {% set tested_table_name = res.node.compiled_code.replace('\n', ' ').replace('`','') %}
      {% set start_index = tested_table_name.find('bin-') %}
      {% set end_index = tested_table_name.find(' ', start_index) %}
      {% set tested_table_name = tested_table_name[start_index:end_index] %}
      {% set tested_table_name = tested_table_name.split('.', 1)[1] %}
      {# Extract costs details #}
      {% set test_billed_bytes = res.adapter_response.bytes_billed %}
      {# Counter increment #}
      {% set total_billed_data_counter.tc = total_billed_data_counter.tc + test_billed_bytes %}
      {# Calculate test cost #}
      {% set test_processed_cost = (test_billed_bytes / TiB) * cost_per_TiB %}
      {# If billed data cost is at least 0.5$, we log its details in the summary #}
        {% if test_processed_cost >= 0.5 %}
          {% set line -%}
            Tested table name: {{ tested_table_name }}
            Test name: {{ test_name }}
            Processed : {{ (test_billed_bytes / GiB) | round(2) }} GiB | Billed : {{ test_processed_cost | round(2) }} $
          {%- endset %}
          {{ log(line, info=True) }}
        {% endif %}
    {% endfor %}
    {# Total costs summary #}
    {{ log("========== Totals costs summary ==========", info=True) }}
    {% set total_billed_data_cost = (total_billed_data_counter.tc / TiB) * cost_per_TiB %}
    {% set line -%}
      {% if total_billed_data_cost > 0.01 -%}
        Total billed data : {{ (total_billed_data_counter.tc / GiB) | round(2) }} GiB | Total cost : {{ total_billed_data_cost | round(2) }} $
        {%- else -%}
        Total billed data : {{ (total_billed_data_counter.tc / MiB) | round(2) }} MiB | Total cost < 0.01$
      {% endif %}
    {%- endset %}
    {{ log(line, info=True) }}
    {{ log("========== End of summary ==========", info=True) }}
  {% endif %}
{% endmacro %}

Notez qu'il vous faudra ajuster la section “Extract tested table name” pour matcher vos noms de projets. Mais, en avez-vous seulement besoin ?🙂

Ajoutez simplement la macro dans votre dossier macros, et vous êtes prêts ! Pour l'exécuter à la fin de chaque exécution dbt, déclarez-la dans votre fichier dbt_project.yml et passez la variable "results" en argument.

# Macro to display the costs of dbt tests. Add this line in your dbt_project.yml
on-run-end: "{{ dbt_test_costs_summary(results) }}"

Chaque fois que dbt est exécuté, le résumé affichera d'abord les tests coûteux (≥ 0,50$), puis fournira un résumé global des coûts des tests.

17:03:17  Running with dbt=1.8.1
17:03:20  Registered adapter: bigquery=1.8.1
17:04:32  Found 1791 models, 20 snapshots, 6421 data tests, 44 seeds, 558 sources, 1204 macros
[..............]

17:05:53  Running 1 on-run-end hook
17:05:53  ========== Expensive tests costs summary ==========
17:05:53  Tested table name: my_dataset.my_big_table
17:05:53  Test name: dbt_utils_unique_combination_of_columns_my_big_table__field_a__field_b__field_c
17:05:53  Processed : 640.81 GiB | Billed : 3.13 $
17:05:53  ========== Totals costs summary ==========
17:05:53  Total billed data : 708.65 GiB | Total cost : 3.46 $
17:05:53  ========== End of summary ==========
17:05:53  1 of 1 START hook: lec_bigdata.on-run-end.0 ......................................... [RUN]
17:05:53  1 of 1 OK hook: lec_bigdata.on-run-end.0 ............................................ [OK in 0.00s]
17:05:53
17:05:53  
17:05:53  Finished running 356 data tests, 1 project hook in 0 hours 0 minutes and 51.70 seconds (51.70s).

Pourquoi c'est important

Rendre les coûts des tests visibles permet aux développeurs de :

  • Identifier les tests coûteux et les optimiser ou les ajuster.
  • Adapter les stratégies de test, par exemple en limitant la portée des tests sur de grands champs ou en réduisant leur fréquence.
  • Prendre des mesures proactives pendant le développement pour gérer les coûts, plutôt que d’être surpris par les factures mensuelles de l’entrepôt de données.

Le retour d’information instantané dans le terminal aide les développeurs à rester conscients de l’impact financier de leurs tests au fur et à mesure qu’ils travaillent. Cette sensibilisation favorise de meilleures pratiques et permet aux équipes de trouver un équilibre entre des tests approfondis et une gestion efficace des coûts.

Un pas vers la transparence des coûts

L’intégration du reporting des coûts de test dans le terminal élimine toute ambiguïté concernant les dépenses liées aux tests dbt. Cela permet aux équipes data de :

  • Optimiser leur approche de test.
  • Maintenir des normes de qualité des données élevées.
  • Gérer efficacement les budgets sans compromettre les objectifs analytiques.

Conclusion

Comprendre les coûts associés aux tests dbt est essentiel pour tout projet data souhaitant rester efficace et soucieux des coûts. Avec une macro aussi simple, les équipes peuvent obtenir une visibilité en temps réel sur les coûts des tests, permettant aux développeurs de prendre des décisions éclairées. Chaque test que vous créez a un coût — le voir dans le terminal est la première étape pour mieux le maîtriser.

Êtes-vous prêt à mettre en lumière les coûts de vos tests dbt ?

Dernier