Adrienne Vermorel
Optimisation des coûts BigQuery : le guide 80/20
Shopify a découvert une requête qui leur aurait coûté 949 000 $ par mois. La solution ? Ajouter du clustering sur les colonnes utilisées dans les clauses WHERE. Ce simple changement a réduit les octets scannés de 75 Go à 508 Mo, une amélioration de 150x grâce à une configuration qui prend quelques minutes à implémenter.
Cette histoire illustre l’essence de l’optimisation des coûts BigQuery : les gains les plus importants proviennent d’un petit ensemble de techniques que la plupart des équipes ignorent ou configurent mal. Pendant ce temps, les équipes passent des heures à débattre des niveaux de stockage alors que 85-90% de leur facture BigQuery provient des coûts de calcul.
Ce guide couvre les 20% d’optimisations qui génèrent 80% des économies. Nous passerons les bases (vous savez probablement déjà ce qu’est le partitionnement). Nous nous concentrerons plutôt sur les configurations qui font vraiment la différence, les anti-patterns qui créent des factures surprises de 20 000 $, et les stratégies spécifiques à dbt qui restent sous-explorées dans la plupart des guides BigQuery.
Partie 1 : Comprendre où va réellement votre argent
La répartition des coûts BigQuery que la plupart des équipes comprennent mal
Les coûts BigQuery se répartissent en trois catégories : calcul (octets scannés), stockage et ingestion en streaming. Voici ce que la plupart des équipes ratent : le calcul représente généralement 85-90% des dépenses totales. Pourtant, les efforts d’optimisation commencent souvent par le nettoyage du stockage.
Les mathématiques sont claires. À 6,25 $ par Tio pour les requêtes à la demande, scanner 10 To quotidiennement coûte 1 875 $ par mois en calcul seul. Ces mêmes 10 To stockés coûtent 200 $ par mois. Une réduction de 50% des octets scannés économise 937 $ mensuellement ; une réduction de 50% du stockage économise 100 $.
Le modèle mental clé : BigQuery facture les données lues, pas les données retournées. Ajouter LIMIT 1000 à une requête ne réduit pas les coûts car BigQuery scanne quand même l’ensemble du dataset avant d’appliquer la limite. Sélectionner 5 colonnes au lieu de 50 dans une table large peut réduire les coûts de 90%. La taille du résultat est sans importance ; c’est la taille du scan qui détermine votre facture.
Comment trouver vos gouffres financiers
Avant d’optimiser quoi que ce soit, identifiez où va réellement l’argent. Cette requête fait ressortir vos 10 requêtes les plus coûteuses des 30 derniers jours :
SELECT user_email, query, total_bytes_processed, ROUND(total_bytes_processed / POW(1024, 4), 2) AS tib_processed, ROUND(total_bytes_processed / POW(1024, 4) * 6.25, 2) AS estimated_cost_usd, creation_time, total_slot_msFROM `region-us`.INFORMATION_SCHEMA.JOBS_BY_PROJECTWHERE creation_time > TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 30 DAY) AND job_type = 'QUERY' AND state = 'DONE' AND total_bytes_processed > 0ORDER BY total_bytes_processed DESCLIMIT 10;Pour l’attribution des coûts par utilisateur ou compte de service, agrégez les données :
SELECT user_email, COUNT(*) AS query_count, ROUND(SUM(total_bytes_processed) / POW(1024, 4), 2) AS total_tib, ROUND(SUM(total_bytes_processed) / POW(1024, 4) * 6.25, 2) AS total_cost_usdFROM `region-us`.INFORMATION_SCHEMA.JOBS_BY_PROJECTWHERE creation_time > TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 30 DAY) AND job_type = 'QUERY' AND state = 'DONE'ORDER BY total_cost_usd DESCLIMIT 20;Exécutez ces requêtes hebdomadairement. Les résultats révèlent souvent que 3-5 requêtes ou utilisateurs représentent 70%+ des dépenses de calcul. Concentrez vos efforts d’optimisation là en premier.
Partie 2 : Les trois leviers qui contrôlent 90% des coûts de requête
Partitionnement : votre première ligne de défense
Le partitionnement divise une table en segments basés sur les valeurs d’une colonne, permettant à BigQuery d’ignorer entièrement les segments non pertinents. Une table bien partitionnée avec des filtres de requête appropriés réduit typiquement les octets scannés de 70-90%.
Quand partitionner : Tables dépassant 100 Go avec des patterns de filtrage cohérents sur une colonne spécifique. Le recommender de Google ne suggère le partitionnement que pour les tables au-dessus de ce seuil car les tables plus petites n’en bénéficient pas assez pour justifier la surcharge de métadonnées.
Types de partition :
- Partitionnement par unité de temps (DAY, MONTH, YEAR) : Idéal pour les données d’événements, logs et transactions où vous filtrez sur un timestamp
- Partitionnement par plage d’entiers : Utile pour le filtrage basé sur des ID quand le temps n’est pas pertinent
- Partitionnement par temps d’ingestion : Partitionne par temps de chargement plutôt que par contenu des données ; utile quand les données source n’ont pas de timestamp fiable
La limite des 10 000 partitions : BigQuery autorise un maximum de 10 000 partitions par table. Le partitionnement quotidien couvre environ 27 ans ; le partitionnement horaire épuise la limite en environ 14 mois. Choisissez la granularité en fonction de votre période de rétention et de vos patterns de requête.
Création d’une table partitionnée avec filtres de partition obligatoires :
CREATE TABLE `project.dataset.events`( event_id STRING, user_id STRING, event_type STRING, event_timestamp TIMESTAMP, properties JSON)PARTITION BY DATE(event_timestamp)CLUSTER BY user_id, event_typeOPTIONS ( require_partition_filter = true);L’option require_partition_filter = true empêche les scans accidentels de table complète. Les requêtes sans filtre sur la colonne de partition échoueront plutôt que de tout scanner.
Dans dbt, configurez le partitionnement dans votre modèle :
{{ config( materialized='table', partition_by={ "field": "event_timestamp", "data_type": "timestamp", "granularity": "day" }, require_partition_filter=true) }}
SELECT event_id, user_id, event_type, event_timestamp, propertiesFROM {{ source('raw', 'events') }}Clustering : le multiplicateur composé
Le clustering trie les données au sein des partitions (ou à travers toute la table pour les tables non partitionnées) basé sur jusqu’à quatre colonnes. Contrairement au partitionnement, le clustering ne crée pas de limites strictes. Il organise les données pour minimiser les octets scannés lors du filtrage sur les colonnes clusterisées.
Une table peut être à la fois partitionnée et clusterisée, et cette combinaison donne les meilleurs résultats. Le partitionnement fournit un filtrage grossier (ignorer des plages de dates entières), tandis que le clustering fournit un filtrage fin au sein de ces partitions (ignorer les blocs qui ne contiennent pas les user_ids pertinents).
L’ordre des colonnes compte significativement. BigQuery priorise la première colonne de clustering, puis la seconde au sein des blocs de la première, et ainsi de suite. Placez votre colonne la plus fréquemment filtrée en premier.
Taille minimum de table : Le clustering bénéficie aux tables plus grandes que 64 Mo. Les tables plus petites tiennent de toute façon dans un seul bloc, rendant le clustering inutile.
Le piège du dry run : Contrairement au partitionnement, le clustering ne fournit pas d’estimations de coût précises dans les dry runs. Un dry run pourrait estimer 50 Go scannés, mais la requête réelle ne scanne que 5 Go grâce au clustering. Les estimations conservatrices ne devraient pas vous décourager de clusteriser.
CREATE TABLE `project.dataset.user_events`( event_date DATE, user_id STRING, event_type STRING, event_name STRING, properties JSON)PARTITION BY event_dateCLUSTER BY user_id, event_type;Dans dbt, combinez partitionnement et clustering :
{{ config( materialized='incremental', partition_by={ "field": "event_date", "data_type": "date" }, cluster_by=["user_id", "event_type"], require_partition_filter=true) }}La fameuse réduction de coûts de Shopify provenait entièrement de l’ajout de clustering. Leur requête filtrait sur des colonnes qui n’étaient pas clusterisées, forçant BigQuery à scanner 75 Go. Après avoir ajouté le clustering sur ces colonnes de filtre, les scans sont tombés à 508 Mo : une réduction de 150x qui a évité 949 000 $ de coûts mensuels.
Sélection des colonnes : le multiplicateur oublié
Le stockage en colonnes de BigQuery signifie que chaque colonne est stockée séparément. Sélectionner les 50 colonnes d’une table coûte 50x plus que sélectionner une seule colonne. Cela fait de SELECT * l’un des patterns les plus coûteux dans BigQuery.
Considérez une table de 5 To avec 10 colonnes de taille à peu près égale :
| Requête | Octets scannés | Coût |
|---|---|---|
SELECT * | 5 To | 31,25 $ |
SELECT col1, col2 | 1 To | 6,25 $ |
SELECT col1 | 500 Go | 3,13 $ |
Le mythe du LIMIT : Beaucoup d’équipes supposent que LIMIT 1000 réduit les coûts. BigQuery scanne d’abord l’ensemble du dataset, puis applique la limite sur la sortie. Cette requête coûte exactement la même chose que vous limitiez à 10 lignes ou 10 millions :
-- Les deux requêtes scannent des octets identiquesSELECT * FROM large_table LIMIT 10;SELECT * FROM large_table LIMIT 10000000;**Alternatives pratiques à SELECT *** :
- Onglet de prévisualisation : L’onglet de prévisualisation de la console BigQuery affiche des données d’exemple sans aucun coût de requête (totalement gratuit)
- TABLESAMPLE : Pour l’échantillonnage statistique,
SELECT * FROM table TABLESAMPLE SYSTEM (1 PERCENT)ne scanne que 1% des données - Listes explicites de colonnes : Toujours spécifier les colonnes dans les requêtes de production
Dans les modèles dbt, évitez le pattern paresseux de tout sélectionner depuis les modèles de base :
-- Coûteux : scanne toutes les colonnes du modèle de baseSELECT * FROM {{ ref('base__analytics__events') }}
-- Mieux : colonnes explicites, seulement ce qui est nécessaireSELECT event_id, user_id, event_timestamp, event_typeFROM {{ ref('base__analytics__events') }}Partie 3 : Coûts de stockage (les changements que la plupart des guides ratent)
Facturation physique vs logique
En juillet 2023, Google a introduit la facturation par octets physiques comme alternative à la facturation logique par défaut. Ce changement reste sous-exploré dans la plupart des guides d’optimisation mais peut générer 30-50% d’économies de stockage.
Facturation logique (par défaut) : Vous payez pour la taille des données non compressées à 0,02 $/Go par mois. Le time travel et le stockage fail-safe sont inclus gratuitement.
Facturation physique : Vous payez pour les données compressées à 0,04 $/Go par mois. Le time travel (7 jours) et le fail-safe (7 jours) sont facturés séparément.
La facturation physique gagne quand votre ratio de compression dépasse 2:1. Puisque le physique coûte 2x par Go mais que les données se compressent typiquement 6-17x, la plupart des organisations économisent significativement en basculant.
Interrogez votre ratio de compression réel avant de décider :
SELECT table_schema, table_name, total_logical_bytes / POW(1024, 3) AS logical_gb, total_physical_bytes / POW(1024, 3) AS physical_gb, ROUND(total_logical_bytes / total_physical_bytes, 2) AS compression_ratio, -- Si le ratio > 2, la facturation physique économise de l'argent CASE WHEN total_logical_bytes / total_physical_bytes > 2 THEN 'Passer au physique' ELSE 'Garder le logique' END AS recommendationFROM `project.region-us`.INFORMATION_SCHEMA.TABLE_STORAGEWHERE total_logical_bytes > 0ORDER BY total_logical_bytes DESC;Compromis à considérer : Avec la facturation physique, le time travel et le fail-safe apparaissent comme des postes séparés. Pour les tables avec des mises à jour fréquentes (qui génèrent une surcharge de time travel), cela peut compenser les économies de compression.
Stockage long terme : réduction gratuite de 50% après 90 jours
BigQuery transfère automatiquement les données vers le stockage long terme après 90 jours consécutifs sans modification. Le prix passe de 0,02 $/Go à 0,01 $/Go : une réduction de 50% ne nécessitant aucune configuration.
Ce qui réinitialise le compteur de 90 jours :
- Toute opération DML (INSERT, UPDATE, DELETE, MERGE)
- Les insertions en streaming
- CREATE OR REPLACE TABLE
Ce qui NE réinitialise PAS le compteur :
- Interroger la table
- Créer des vues sur la table
- Exporter des données
- Copier la table
Pour les tables partitionnées, chaque partition est évaluée indépendamment. Une table avec 365 partitions quotidiennes aura ~275 partitions au tarif long terme et ~90 au tarif actif, en supposant que seules les données récentes reçoivent des écritures.
Expiration des tables : prévenir l’accumulation de stockage
Les tables temporaires, artefacts ETL et expérimentations de développement s’accumulent silencieusement. Une startup a découvert qu’elle payait 3 000 $ mensuellement pour des tables temporaires que personne n’avait interrogées depuis des mois.
Définissez des valeurs par défaut au niveau du dataset :
ALTER SCHEMA `project.staging`SET OPTIONS ( default_table_expiration_days = 7);Surchargez au niveau de la table si nécessaire :
CREATE TABLE `project.staging.temp_analysis`OPTIONS ( expiration_timestamp = TIMESTAMP_ADD(CURRENT_TIMESTAMP(), INTERVAL 24 HOUR)) ASSELECT * FROM source_table WHERE condition;Dans dbt, utilisez la config hours_to_expiration pour les modèles temporaires :
{{ config( materialized='table', hours_to_expiration=168 -- 7 jours) }}Partie 4 : Quand passer de la demande aux slots
Les mathématiques du seuil de rentabilité
La tarification par Éditions de BigQuery (introduite en juillet 2023) a remplacé le forfait legacy par trois niveaux :
| Édition | Coût par slot-heure | Idéal pour |
|---|---|---|
| Standard | 0,04 $ | Dev/test, ad-hoc |
| Enterprise | 0,06 $ | Production, workloads ML |
| Enterprise Plus | 0,10 $ | Industries réglementées, DR |
100 slots fonctionnant en continu sur l’Édition Standard PAYG coûtent 2 920 $ mensuellement. À 6,25 $ par Tio à la demande, cela équivaut à traiter 467 To. Si vous traitez plus de 400-500 To mensuellement avec des patterns cohérents, les slots économisent probablement de l’argent.
Mais le calcul change dramatiquement pour les workloads en rafale. Traiter 20 To en moins d’une heure :
- À la demande : 125 $ (20 × 6,25 $)
- 100 slots pour 1 heure (Standard) : ~5 $
C’est 95% d’économies pour les scénarios en rafale, rendant les slots attractifs même à des volumes mensuels plus faibles si les workloads se concentrent dans des fenêtres.
Stratégie d’autoscaling
L’autoscaling est devenu généralement disponible en février 2025, permettant une allocation dynamique de slots qui scale de zéro jusqu’à votre maximum configuré.
Décisions de configuration clés :
Slots de base : Définissez à zéro pour les workloads avec des périodes d’inactivité. Les slots scalent depuis zéro instantanément sans pénalité de démarrage. Payez uniquement pour ce que vous utilisez.
Slots maximum : Votre plafond pendant la demande de pointe. Commencez prudemment et augmentez en fonction des temps d’attente observés.
Utilisation cible : Visez 60-80% pendant les heures de pointe. Constamment plus élevé indique un sous-approvisionnement (requêtes en file d’attente) ; constamment plus bas suggère un sur-approvisionnement (paiement pour une capacité inutilisée).
-- Créer une réservation avec autoscalingCREATE RESERVATION `project.region-us.prod_reservation`OPTIONS ( slot_capacity = 0, -- baseline edition = 'ENTERPRISE', autoscale = ( max_slots = 500 ));
-- Créer une affectation pour votre projetCREATE ASSIGNMENT `project.region-us.prod_reservation.prod_assignment`OPTIONS ( assignee = 'projects/my-project', job_type = 'QUERY');Remises sur engagement (nouveauté 2025)
Google Cloud Next 2025 a introduit des remises sur engagement basées sur les dépenses pour BigQuery :
- Engagement 1 an : 10% de réduction par rapport au PAYG
- Engagement 3 ans : 20% de réduction par rapport au PAYG
Contrairement aux engagements basés sur les slots, ceux-ci sont libellés en dollars et flexibles entre les Éditions BigQuery, Cloud Composer 3 et les produits Data Governance. Cela simplifie la planification de capacité : engagez-vous sur un niveau de dépenses mensuelles plutôt que de deviner les comptages de slots.
Pour les workloads prévisibles, combinez un engagement pour la capacité de base avec l’autoscaling pour les rafales. Vous capturez des économies sur l’utilisation prévisible tout en maintenant la flexibilité pour les pics.
Comparaison des fonctionnalités par édition
Au-delà du prix, les éditions diffèrent en capacités :
Édition Standard :
- Maximum 1 600 slots
- Pas de BigQuery ML
- Pas de vues matérialisées avec rafraîchissement automatique
- Idéale pour : Développement, analyse ad-hoc, workloads sensibles aux coûts
Édition Enterprise :
- Slots illimités (avec quota approprié)
- BigQuery ML inclus
- Accélération BI Engine
- Recherche full-text
- SLA 99,99%
- Idéale pour : Workloads de production, pipelines ML
Édition Enterprise Plus :
- Tout ce qui est dans Enterprise
- Reprise après sinistre multi-région
- Certifications de conformité (FedRAMP, CJIS, IL4)
- Idéale pour : Industries réglementées, workloads critiques
Note importante : La tarification à la demande maintient la parité des fonctionnalités avec Enterprise Plus (sauf les requêtes continues). Si vous avez besoin de fonctionnalités de conformité mais avez une utilisation imprévisible, la demande peut être plus rentable que les slots Enterprise Plus.
Partie 5 : Optimisations spécifiques à dbt
La plupart des guides traitent dbt et l’optimisation des coûts BigQuery séparément. Pourtant, les résultats réels des clients démontrent l’impact : Bilt Rewards a réduit de 20 000 $ mensuellement, Enpal a réduit ses dépenses data de 70%, et Symend a diminué l’utilisation quotidienne de son data warehouse de 70% grâce à des configurations dbt appropriées.
Impact de la matérialisation sur les coûts
Votre choix de matérialisation détermine les coûts de base pour chaque modèle :
Table : Un scan complet unique lors de la construction, puis les coûts de stockage. Chaque requête en aval lit depuis la table matérialisée gratuitement (pas de recalcul).
View : Zéro coût de construction, zéro stockage, mais scan complet en amont à chaque requête. Cela devient un piège à coûts pour les transformations complexes ou les modèles fréquemment interrogés.
Ephemeral : Pas de stockage, interpolé comme un CTE. Les CTE référencés plusieurs fois s’exécutent une fois par référence, potentiellement doublant les coûts. Les modèles éphémères sont aussi difficiles à déboguer car ils n’existent pas comme objets interrogeables.
L’anti-pattern de la chaîne de vues : dbt_project_evaluator signale les chaînes de 4+ vues comme problématiques. Rien ne calcule jusqu’à ce qu’une requête atteigne une table, déclenchant l’exécution en cascade de toute la logique des vues en amont. Cinq vues chaînées ne coûtent pas 5x ; elles peuvent coûter 5x sur chaque requête en aval.
Étant donné que les coûts de stockage sont négligeables par rapport au calcul (Partie 1), utiliser les tables par défaut a du sens :
models: my_project: base: +materialized: table intermediate: +materialized: table marts: +materialized: table +partition_by: field: created_date data_type: dateLes vues peuvent fonctionner pour des transformations vraiment simples qui sont rarement interrogées, mais les tables éliminent le risque de coûts de calcul en cascade.
Modèles incrémentaux : le gain le plus important
Les rafraîchissements complets de tables scannent et réécrivent des tables entières à chaque exécution. Pour une table de 500 Go exécutée quotidiennement, c’est 3,13 $ par exécution, 94 $ mensuellement, juste pour la reconstruction avant toute requête.
Les modèles incrémentaux ne traitent que les données nouvelles ou modifiées. Les économies scalent avec la taille de la table et la fréquence d’exécution.
Le piège de la stratégie par défaut : La stratégie incrémentale par défaut de BigQuery est merge, qui scanne toute la table de destination à chaque exécution pour identifier les lignes correspondantes. Pour les grandes tables, cela annule l’objectif.
Passez à insert_overwrite pour les tables partitionnées :
{{ config( materialized='incremental', incremental_strategy='insert_overwrite', partition_by={ "field": "event_date", "data_type": "date" }, cluster_by=["user_id", "event_type"]) }}
SELECT event_date, user_id, event_type, COUNT(*) as event_countFROM {{ source('raw', 'events') }}WHERE event_date >= DATE_SUB(CURRENT_DATE(), INTERVAL 3 DAY)GROUP BY 1, 2, 3En utilisant insert_overwrite, ajoutez copy_partitions: true pour utiliser l’API Copy Table de BigQuery au lieu des instructions INSERT :
{{ config( materialized='incremental', incremental_strategy='insert_overwrite', partition_by={"field": "event_date", "data_type": "date"}, partitions=["date_sub(current_date, interval 1 day)", "current_date"], copy_partitions=true) }}Le piège is_incremental() : Le pattern dbt courant pour les modèles incrémentaux cause toujours des scans complets de la table source :
-- Ce pattern scanne toute la table source !{% if is_incremental() %}WHERE event_timestamp > (SELECT MAX(event_timestamp) FROM {{ this }}){% endif %}La sous-requête SELECT MAX(event_timestamp) FROM {{ this }} n’aide pas BigQuery à élaguer les partitions dans la table source. Utilisez des valeurs de partition statiques ou la variable _dbt_max_partition à la place :
{{ config( materialized='incremental', incremental_strategy='insert_overwrite', partition_by={"field": "event_date", "data_type": "date"}) }}
SELECT *FROM {{ source('raw', 'events') }}{% if is_incremental() %}-- La référence de partition statique permet l'élagageWHERE event_date >= DATE_SUB(CURRENT_DATE(), INTERVAL 3 DAY){% endif %}Ou utilisez la variable de partition intégrée de dbt :
{% if is_incremental() %}WHERE event_date >= DATE('{{ var("_dbt_max_partition") }}'){% endif %}Attribution des coûts avec dbt
Suivre les coûts par modèle nécessite d’étiqueter les requêtes avec des métadonnées. Configurez dbt pour ajouter des labels de job :
query-comment: comment: "dbt: {{ node.unique_id }}" append: true
# Ou plus détaillé :query-comment: comment: | { "dbt_model": "{{ node.unique_id }}", "dbt_invocation_id": "{{ invocation_id }}", "dbt_target": "{{ target.name }}" } append: truePuis interrogez les coûts par modèle :
SELECT JSON_EXTRACT_SCALAR(query, '$.dbt_model') AS dbt_model, COUNT(*) AS runs, ROUND(SUM(total_bytes_processed) / POW(1024, 4), 4) AS total_tib, ROUND(SUM(total_bytes_processed) / POW(1024, 4) * 6.25, 2) AS total_cost_usdFROM `region-us`.INFORMATION_SCHEMA.JOBS_BY_PROJECTWHERE creation_time > TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 7 DAY) AND query LIKE '%dbt_model%'GROUP BY 1ORDER BY total_cost_usd DESC;Pour un monitoring complet, le package communautaire dbt-bigquery-monitoring automatise ce pattern à travers les projets et régions.
Partie 6 : Les erreurs coûteuses
Les forums communautaires révèlent des patterns d’erreurs BigQuery coûteuses qui diffèrent de ce que les guides d’optimisation couvrent typiquement. Un fil Hacker News sur les coûts BigQuery a fait émerger des praticiens partageant des histoires de surprises de 8 000 à 20 000 $ sur un seul mois.
Le mythe de la clause LIMIT
Cette idée fausse cause le plus de factures inattendues. Les tests confirment : SELECT * FROM table LIMIT 100 scanne la table entière. LIMIT n’affecte que la taille de sortie, pas les octets traités.
L’exemple de Google lui-même : si 10 utilisateurs exécutent des requêtes SELECT * 10 fois par mois sur une table de 10 To, les coûts de calcul passent de 200 $ (stockage seul) à plus de 5 000 $ mensuellement.
Utilisez l’onglet de prévisualisation (totalement gratuit), appliquez des filtres de partition, ou sélectionnez uniquement les colonnes nécessaires. N’utilisez jamais LIMIT comme mécanisme de contrôle des coûts.
Double comptage des CTE
BigQuery ne matérialise pas les CTE non récursifs. Chaque référence réexécute le scan sous-jacent. Un CTE référencé deux fois coûte deux fois plus.
-- Ceci scanne la table de base DEUX FOISWITH user_aggregates AS ( SELECT user_id, complex_aggregation FROM base__analytics__user_events GROUP BY user_id)SELECT a.user_id, a.complex_aggregation, b.complex_aggregationFROM user_aggregates aJOIN user_aggregates b ON a.user_id = b.user_id; -- Deuxième scan !Matérialisez les CTE coûteux dans des tables temporaires :
-- Matérialiser dans une table temp (scanne une fois)CREATE TEMP TABLE user_aggregates ASSELECT user_id, complex_aggregationFROM base__analytics__user_eventsGROUP BY user_id;
-- Maintenant les deux références lisent depuis la table temp (pas cher)SELECT a.user_id, a.complex_aggregation, b.complex_aggregationFROM user_aggregates aJOIN user_aggregates b ON a.user_id = b.user_id;Streaming vs chargement par batch
Les insertions en streaming coûtent 0,01 $ par 200 Mo avec un minimum de 1 Ko par ligne. Le chargement par batch utilisant le pool de slots partagés de BigQuery est totalement gratuit.
Pour une table recevant 10 Go de données quotidiennement :
- Streaming : 0,50 $/jour, 15 $/mois
- Chargement par batch : 0 $/mois
Un praticien a documenté une réduction de 95% des coûts BigQuery en passant du streaming aux chargements de fichiers avec du micro-batching.
Quand le streaming est justifié :
- Exigences temps réel véritables (latence inférieure à la minute)
- Données qui perdent de la valeur après des minutes, pas des heures
- Systèmes où l’orchestration par batch ajoute une complexité inacceptable
Quand utiliser le batch à la place :
- Tolérance de latence de 15+ minutes
- Ingestion à grand volume (les coûts de streaming scalent linéairement)
- Patterns de rafraîchissement de data warehouse (horaire, quotidien)
Filtres de partition manquants
La même requête sur la même table peut coûter 17x plus selon la colonne sur laquelle vous filtrez :
-- Scanne uniquement les partitions pertinentes : 884 MoSELECT * FROM eventsWHERE event_date = '2024-01-15';
-- Scanne toute la table : 15 Go (17x plus cher !)SELECT * FROM eventsWHERE DATE(event_timestamp) = '2024-01-15';La deuxième requête applique une fonction à la colonne de partition, empêchant BigQuery d’élaguer les partitions.
Patterns de filtre courants qui cassent l’élagage :
-- MAUVAIS : Fonction sur la colonne de partitionWHERE DATE(created_timestamp) = '2024-01-15'WHERE EXTRACT(YEAR FROM event_date) = 2024
-- BON : Comparaison directeWHERE created_timestamp BETWEEN '2024-01-15' AND '2024-01-16'WHERE event_date = '2024-01-15'
-- MAUVAIS : OR avec une colonne non-partitionWHERE event_date = '2024-01-15' OR user_id = 'abc'
-- BON : Requêtes séparées ou restructurer la logiqueWHERE event_date = '2024-01-15'Self-joins
La documentation de Google avertit explicitement : les self-joins “peuvent mettre au carré le nombre de lignes de sortie”, faisant exploser les coûts de manière imprévisible.
-- Self-join : scanne potentiellement la table deux fois, les lignes de sortie peuvent exploserSELECT a.user_id, a.event_timestamp AS first_event, b.event_timestamp AS next_eventFROM events aJOIN events b ON a.user_id = b.user_id AND b.event_timestamp > a.event_timestamp;Remplacez par des fonctions de fenêtre :
-- Fonction de fenêtre : un seul scan, sortie prévisibleSELECT user_id, event_timestamp AS first_event, LEAD(event_timestamp) OVER ( PARTITION BY user_id ORDER BY event_timestamp ) AS next_eventFROM events;Les fonctions de fenêtre (LEAD, LAG, ROW_NUMBER, RANK) accomplissent la plupart des cas d’usage de self-join avec un seul scan de table.
Partie 7 : Gouvernance et garde-fous
Pour les organisations avec des utilisateurs d’analytique ad-hoc, les contrôles de gouvernance peuvent générer plus d’économies que les optimisations techniques. Un seul analyste exécutant des requêtes SELECT * non filtrées peut générer des factures dépassant des mois d’efforts d’ingénierie sur le partitionnement.
Contrôles au niveau requête
max_bytes_billed définit une limite stricte par requête. Les requêtes qui dépasseraient cette limite échouent avant de scanner les données :
-- La requête échoue si elle scannerait plus de 10 GoSELECT * FROM large_tableOPTIONS (max_bytes_billed = 10737418240); -- 10 Go en octetsDans dbt, configurez dans votre profil :
my_project: target: prod outputs: prod: type: bigquery method: oauth project: my-project dataset: production maximum_bytes_billed: 107374182400 # 100 GoPour les exécutions dbt, cela empêche tout modèle individuel de scanner plus que la limite spécifiée. Utile pour détecter les modèles incrémentaux mal configurés qui exécutent accidentellement des rafraîchissements complets.
Quotas au niveau projet
Depuis septembre 2025, les nouveaux projets BigQuery ont par défaut un quota quotidien de 200 Tio. Pour les projets existants, configurez des quotas personnalisés via l’Admin SDK ou la Cloud Console.
Considérez implémenter :
- Limites quotidiennes par utilisateur : Empêcher les analystes individuels de monopoliser le budget
- Limites par projet : Plafonner les dépenses sur les projets de développement vs production
- Seuils d’alerte : Notifier avant d’atteindre les limites strictes
Patterns d’accès qui préviennent les erreurs coûteuses
Comptes de service par intégration : Créez des comptes de service séparés pour dbt, Looker, Fivetran, etc. Interrogez INFORMATION_SCHEMA par email de compte de service pour attribuer les coûts par système.
**Vues autorisées pour la protection SELECT *** : Au lieu d’accorder un accès direct aux tables, créez des vues qui n’exposent que les colonnes nécessaires :
CREATE VIEW `project.views.user_summary` ASSELECT user_id, signup_date, user_typeFROM `project.raw.users`;
-- Accorder l'accès à la vue, pas à la tableGRANT roles/bigquery.dataViewerON `project.views.user_summary`TO 'user:analyst@company.com';Affectations de réservation pour l’isolation de capacité : Affectez différentes équipes à des réservations séparées. Cela empêche le workload lourd d’une équipe de consommer les slots d’une autre équipe.
Partie 8 : Monitoring et optimisation continue
Les requêtes à exécuter hebdomadairement
Top 10 des requêtes coûteuses cette semaine :
SELECT user_email, SUBSTR(query, 1, 200) AS query_preview, ROUND(total_bytes_processed / POW(1024, 4), 4) AS tib_scanned, ROUND(total_bytes_processed / POW(1024, 4) * 6.25, 2) AS cost_usd, creation_timeFROM `region-us`.INFORMATION_SCHEMA.JOBS_BY_PROJECTWHERE creation_time > TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 7 DAY) AND job_type = 'QUERY' AND total_bytes_processed > 0ORDER BY total_bytes_processed DESCLIMIT 10;Tendance des coûts par dataset :
SELECT DATE(creation_time) AS date, referenced_table.dataset_id, ROUND(SUM(total_bytes_processed) / POW(1024, 4), 2) AS tib_scanned, ROUND(SUM(total_bytes_processed) / POW(1024, 4) * 6.25, 2) AS cost_usdFROM `region-us`.INFORMATION_SCHEMA.JOBS_BY_PROJECT,UNNEST(referenced_tables) AS referenced_tableWHERE creation_time > TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 30 DAY) AND job_type = 'QUERY'GROUP BY 1, 2ORDER BY date DESC, cost_usd DESC;Tables sans partitionnement ou clustering (candidates à l’optimisation) :
SELECT table_schema, table_name, ROUND(total_logical_bytes / POW(1024, 3), 2) AS size_gb, CASE WHEN partition_column IS NULL THEN 'Manquant' ELSE 'OK' END AS partitioning, CASE WHEN clustering_columns IS NULL THEN 'Manquant' ELSE 'OK' END AS clusteringFROM `project.region-us`.INFORMATION_SCHEMA.TABLE_STORAGELEFT JOIN `project.region-us`.INFORMATION_SCHEMA.PARTITIONS USING (table_schema, table_name)LEFT JOIN ( SELECT table_schema, table_name, STRING_AGG(clustering_ordinal_position) AS clustering_columns FROM `project.region-us`.INFORMATION_SCHEMA.COLUMNS WHERE clustering_ordinal_position IS NOT NULL GROUP BY 1, 2) USING (table_schema, table_name)WHERE total_logical_bytes > 107374182400 -- > 100 GoORDER BY total_logical_bytes DESC;Exploiter les recommandations intégrées de Google
Le recommender de BigQuery suggère automatiquement le partitionnement et le clustering pour les tables qui en bénéficieraient. Accédez aux recommandations via :
- Cloud Console → BigQuery → Détails de la table → Onglet Recommandations
INFORMATION_SCHEMA.TABLE_OPTIONSpour l’accès programmatique- API Recommender pour le traitement automatisé
Le recommender analyse les patterns de requêtes et suggère des colonnes pour le partitionnement/clustering basé sur l’utilisation réelle des filtres, pas des suppositions. La documentation des bonnes pratiques d’optimisation des coûts de Google couvre des techniques supplémentaires.
Construire un dashboard de coûts
Métriques clés à suivre :
- Dépenses de calcul quotidiennes : Détecter les anomalies avant qu’elles ne s’accumulent
- Tendance des octets scannés : Indicateur avancé des changements de coûts
- Utilisation des slots (si vous utilisez des réservations) : Identifier le sous/sur-approvisionnement
- Coût moyen par requête : Suivre le progrès de l’optimisation dans le temps
- Principaux contributeurs aux coûts : Utilisateurs, comptes de service, datasets, modèles
Définissez des seuils d’alerte à :
- 50% du budget quotidien : Notification d’avertissement
- 80% du budget quotidien : Notification critique au responsable d’équipe
- 100% : Processus d’investigation déclenché automatiquement
Conclusion : La hiérarchie d’optimisation
Une hiérarchie claire devrait guider vos priorités d’optimisation :
Priorité 1 : Patterns de requête (80%+ d’impact)
- Sélection des colonnes : Arrêtez d’utiliser SELECT *
- Filtres de partition : Toujours filtrer sur les colonnes de partition
- Clustering : Ajouter aux tables de plus de 64 Mo que vous filtrez fréquemment
Ces changements prennent quelques minutes à implémenter et capturent la majorité des économies.
Priorité 2 : Configurations dbt (70% de potentiel d’économies)
- Passer aux modèles incrémentaux pour les grandes tables à ajout fréquent
- Utiliser la stratégie
insert_overwriteaveccopy_partitions: true - Corriger les patterns is_incremental() qui causent des scans complets
Priorité 3 : Gouvernance (prévient les factures surprises)
- Définir
max_bytes_billedsur tous les projets - Implémenter des quotas quotidiens pour les utilisateurs ad-hoc
- Créer des vues autorisées au lieu d’un accès direct aux tables
Priorité 4 : Modèle de tarification (20-40% d’économies)
- Évaluer les slots si vous traitez > 400 To mensuellement ou des workloads en rafale
- Configurer l’autoscaling avec une base de zéro pour les workloads variables
- Superposer des remises sur engagement pour une base prévisible
Priorité 5 : Stockage (10-30% d’économies)
- Évaluer la facturation physique vs logique en utilisant la requête de ratio de compression
- Définir des politiques d’expiration sur les tables temporaires
- Laisser les remises de stockage long terme fonctionner automatiquement
Par où commencer
Exécutez la requête INFORMATION_SCHEMA de la Partie 1 pour trouver vos 5 requêtes les plus coûteuses. Pour chacune, vérifiez :
- Utilise-t-elle SELECT * alors qu’elle n’a pas besoin de toutes les colonnes ?
- Filtre-t-elle sur la colonne de partition ?
- La table est-elle clusterisée sur les colonnes utilisées dans les clauses WHERE ?
Corriger ces trois problèmes sur vos 5 principales requêtes capture probablement la plupart de vos économies disponibles. Tout le reste est de l’optimisation à la marge.
Annexe : Référence rapide
Aide-mémoire des tarifs (à date de janvier 2026)
| Élément | Coût |
|---|---|
| Requête à la demande | 6,25 $/Tio |
| Stockage actif (logique) | 0,02 $/Go/mois |
| Stockage long terme (logique) | 0,01 $/Go/mois |
| Stockage physique | 0,04 $/Go/mois |
| Insertions en streaming | 0,01 $/200 Mo |
| Slots Édition Standard | 0,04 $/slot-heure |
| Slots Édition Enterprise | 0,06 $/slot-heure |
| Slots Édition Enterprise Plus | 0,10 $/slot-heure |
| BI Engine | 0,0416 $/Go/heure |
Seuils clés
| Décision | Seuil |
|---|---|
| Envisager le partitionnement | Table > 100 Go |
| Envisager le clustering | Table > 64 Mo |
| Envisager les slots plutôt que la demande | > 400-500 To/mois ou workloads en rafale |
| La facturation physique gagne | Ratio de compression > 2:1 |
| Le stockage long terme s’applique | 90 jours sans modification |
Référence rapide des configs dbt
# Modèle incrémental optimisé pour BigQuery{{ config( materialized='incremental', incremental_strategy='insert_overwrite', partition_by={ "field": "event_date", "data_type": "date", "granularity": "day" }, cluster_by=["user_id", "event_type"], require_partition_filter=true, copy_partitions=true) }}