Adrienne Vermorel
Partitioning vs. Clustering : le framework de décision
Bien configurer le partitioning et le clustering peut réduire vos coûts BigQuery de 90 à 99 %. Mal le faire (ou pire, ne pas y penser du tout) signifie que vous brûlez de l’argent à chaque requête.
La plupart des analytics engineers comprennent ce que font le partitioning et le clustering. La question plus difficile est quand utiliser chacun, et comment ils interagissent avec vos modèles dbt. Ce tutoriel fournit un framework de décision pratique basé sur vos patterns de requêtes réels, la taille de vos tables et vos contraintes opérationnelles.
Rappel rapide : comment ils fonctionnent différemment
La différence mécanique fondamentale conditionne tout ce qui suit.
Partitioning : des frontières physiques
Le partitioning divise physiquement votre table en segments basés sur les valeurs d’une seule colonne. Quand vous interrogez avec un filtre sur la colonne de partition, BigQuery effectue le partition pruning : il ignore complètement les partitions qui ne correspondent pas à votre filtre. Ces octets ne sont jamais scannés, jamais facturés.
BigQuery supporte trois types de partitioning :
Le partitioning par colonne temporelle utilise une colonne DATE, TIMESTAMP ou DATETIME avec une granularité HOUR, DAY, MONTH ou YEAR. C’est le choix standard pour les données événementielles et les workloads de séries temporelles.
Le partitioning par temps d’ingestion assigne les lignes aux partitions en fonction du moment où BigQuery les ingère, en utilisant les pseudo-colonnes _PARTITIONTIME et _PARTITIONDATE. Utile quand vos données source manquent de timestamps fiables.
Le partitioning par plage d’entiers divise les données par plages de colonnes INTEGER que vous définissez (début, fin, intervalle). Fonctionne pour les patterns d’accès basés sur des ID ou les scénarios de segmentation client.
Contraintes clés : vous ne pouvez partitionner que par une seule colonne, vous êtes limité à 10 000 partitions par table, et le partition pruning ne fonctionne qu’avec des expressions constantes. Un filtre comme WHERE event_date >= (SELECT MAX(date) FROM other_table) ne prunera pas car la valeur dépend d’une sous-requête.
Clustering : des blocs triés
Le clustering trie les données au sein des blocs de stockage selon jusqu’à quatre colonnes. Cela permet le block-level pruning — BigQuery maintient des métadonnées sur les plages de valeurs dans chaque bloc et ignore les blocs qui ne peuvent pas contenir de lignes correspondantes.
Contrairement au partitioning, le clustering ne crée pas de divisions physiques. BigQuery utilise une approche de type LSM tree : les nouvelles données arrivent dans des blocs “delta” triés localement, et un processus en arrière-plan les fusionne en blocs de base entièrement triés. Ce re-clustering automatique se fait sans coût (pas de consommation de slots, pas de frais).
L’ordre des colonnes est critique. BigQuery trie hiérarchiquement selon vos colonnes dans l’ordre. Les requêtes ne bénéficient du clustering que lorsqu’elles filtrent sur un préfixe des colonnes de clustering. Si vous clusterez par [region, customer_id, product_id] :
- Filtrer sur
regionseul → optimisé - Filtrer sur
regionETcustomer_id→ optimisé - Filtrer sur
customer_idseul, en sautantregion→ non optimisé
Le partitioning crée des frontières strictes qui éliminent des segments entiers. Le clustering organise les données au sein de ces segments pour un filtrage plus fin.
Le framework de décision
Le choix entre partitioning et clustering repose sur trois facteurs : vos patterns de requêtes dominants, la taille des tables, et si vous avez besoin d’opérations au niveau partition (expiration, suppressions ciblées, estimations de coûts prévisibles).
Voici l’arbre de décision :
flowchart TD A{Table > 64 Mo ?} A -->|Non| B[Pas d'optimisation] A -->|Oui| C{80%+ des requêtes filtrent<br>sur une colonne date ?} C -->|Oui| D[Partitionner par date] C -->|Non| E{Filtres haute<br>cardinalité ?} D --> F{Partitions > 10 Go ?} F -->|Oui| G[Partition + Cluster] F -->|Non| H[Partition mensuelle<br>+ Cluster] E -->|Oui| I[Cluster uniquement] E -->|Non| J[Au cas par cas]Utilisez le partitioning seul quand
Vos requêtes filtrent systématiquement sur une seule colonne date/timestamp (80 % ou plus des requêtes incluent un filtre de plage de dates). Le partitioning vous donne :
- Des estimations de coûts prévisibles avant exécution. Le partition pruning vous dit exactement combien d’octets seront scannés à l’avance (les estimations de clustering ne sont finalisées qu’à la fin de la requête).
- La gestion du cycle de vie des partitions. Vous pouvez définir une expiration automatique (
partition_expiration_days), supprimer efficacement des plages de dates entières, ou écrire dans des partitions spécifiques sans toucher aux autres. - Une taille de partition suffisante. Chaque partition devrait atteindre en moyenne au moins 10 Go. Des partitions plus petites créent un overhead de métadonnées sans bénéfice proportionnel.
Utilisez le clustering seul quand
Utilisez le clustering quand le partitioning ne correspond pas à vos patterns d’accès ou à la taille de votre table :
- La table fait entre 64 Mo et 10 Go. Trop petite pour des partitions significatives, mais assez grande pour bénéficier du block pruning.
- Plusieurs colonnes de filtre sans dominante unique. Si les requêtes filtrent sur
user_id,product_id,regionetstatusdans diverses combinaisons, le clustering gère cela ; le partitioning ne peut pas. - Des filtres haute cardinalité. Des colonnes comme
user_idousession_idont trop de valeurs distinctes pour le partitioning (vous atteindriez la limite de 10 000 partitions). Le clustering gère bien la haute cardinalité. - Le partitioning créerait des partitions minuscules. Si le volume de données quotidien est de 100 Mo, les partitions quotidiennes sont un gaspillage. Utilisez le clustering à la place.
Utilisez les deux quand
Combinez les deux quand vous avez de grandes tables avec des patterns de requêtes multidimensionnels :
- Tables dépassant 10 Go par partition où les requêtes filtrent sur la date et d’autres dimensions.
- Tables d’événements où le schéma standard est : partitionner par date de transaction/événement, puis appliquer le clustering sur les clés de dimension couramment filtrées.
- Vous avez besoin de prévisibilité des coûts (partition) plus un filtrage granulaire (cluster) pour les colonnes au-delà de la clé de partition.
Les seuils de taille qui comptent vraiment
La documentation Google indique que les tables ou partitions de plus de 64 Mo bénéficient du clustering (les données plus petites voient une amélioration “négligeable”). Le système Recommender de Google utilise des seuils différents : il suggère le partitioning pour les tables dépassant 100 Go et le clustering pour les tables dépassant 10 Go.
En pratique, 1 Go est le minimum où vous verrez des améliorations significatives du clustering en production.
| Taille table/partition | Recommandation |
|---|---|
| < 64 Mo | Pas d’optimisation |
| 64 Mo – 1 Go | Clustering seulement si filtrage intensif |
| 1 Go – 10 Go | Clustering recommandé |
| 10 Go – 100 Go | Clustering + partitioning |
| > 100 Go | Les deux obligatoires ; forcer les filtres de partition |
Le piège des petites partitions
Le partitioning quotidien avec 100 Mo de données par jour crée 365 petites partitions par an. Chaque partition ajoute un overhead de métadonnées, et BigQuery doit les assembler au moment de la requête.
Corrigez cela en utilisant un partitioning mensuel combiné au clustering sur la colonne date pour la granularité infra-mensuelle. Vous obtenez les avantages de gestion du cycle de vie du partitioning sans l’overhead de centaines de petites partitions.
Patterns de configuration réels
Différents domaines de données appellent différentes stratégies d’optimisation. Voici ce qui fonctionne en pratique.
Données événementielles (GA4, clickstream)
Les exports GA4 créent des tables shardées quotidiennement (events_YYYYMMDD). Pour les tables d’événements consolidées ou les modèles d’analytics personnalisés :
{{ config( materialized='incremental', partition_by={ 'field': 'event_date', 'data_type': 'date', 'granularity': 'day' }, cluster_by=['event_name', 'user_pseudo_id', 'traffic_source'], require_partition_filter=true) }}Pourquoi ça fonctionne : Les requêtes filtrent d’abord par plage de dates (partition pruning), puis descendent par type d’événement et utilisateur (clustering). La colonne de clustering traffic_source permet l’analyse d’attribution sans scans supplémentaires.
Données marketing et publicitaires
Les données Google Ads et Facebook Ads arrivent typiquement avec une hiérarchie de campagne (campaign → ad group → ad) :
{{ config( materialized='incremental', partition_by={ 'field': 'report_date', 'data_type': 'date', 'granularity': 'day' }, cluster_by=['campaign_id', 'ad_group_id', 'ad_id']) }}Pourquoi ça fonctionne : Les marketeurs analysent les performances dans des plages de dates, en descendant dans la hiérarchie de campagne. L’ordre des colonnes de clustering correspond au chemin de drill-down.
SaaS multi-tenant
Pour les tables partagées servant plusieurs clients, la décision critique est où placer tenant_id :
{{ config( materialized='incremental', partition_by={ 'field': 'created_date', 'data_type': 'date', 'granularity': 'day' }, cluster_by=['tenant_id', 'user_id', 'entity_type'], require_partition_filter=true) }}Pourquoi ça fonctionne : tenant_id comme première colonne de clustering assure une isolation efficace des tenants sans nécessiter de tables séparées par client. Toutes les requêtes pour un tenant spécifique bénéficient du block pruning.
Séries temporelles et IoT
Pour les données de capteurs à haut volume avec des requêtes spécifiques aux appareils :
{{ config( materialized='incremental', partition_by={ 'field': 'measurement_date', 'data_type': 'date', 'granularity': 'day' -- ou 'hour' pour très haut volume avec rétention < 6 mois }, cluster_by=['sensor_id', 'device_type', 'location']) }}Pourquoi ça fonctionne : Les requêtes spécifiques aux appareils dans des fenêtres temporelles sont le schéma dominant. Placer sensor_id en première position de clustering gère efficacement la haute cardinalité des identifiants d’appareils.
Configuration dbt-bigquery
L’adaptateur dbt-bigquery fournit une configuration directe pour le partitioning et le clustering.
Configuration de base
{{ config( materialized='incremental', partition_by={ 'field': 'event_date', 'data_type': 'date', 'granularity': 'day' }, cluster_by=['user_id', 'event_type'], require_partition_filter=true, partition_expiration_days=365) }}
SELECT DATE(event_timestamp) AS event_date, user_id, event_type, event_paramsFROM {{ source('analytics', 'raw_events') }}{% if is_incremental() %} WHERE DATE(event_timestamp) >= _dbt_max_partition{% endif %}Pour le partitioning par plage d’entiers :
partition_by={ 'field': 'customer_id', 'data_type': 'int64', 'range': { 'start': 0, 'end': 1000000, 'interval': 1000 }}Interactions avec les stratégies incrémentales
Votre choix de stratégie incrémentale affecte le comportement du partitioning et du clustering pendant les exécutions de modèles.
La stratégie merge (par défaut) effectue un scan complet des tables source et destination pour identifier les lignes correspondantes. Le clustering améliore significativement les performances du merge en réduisant le nombre de lignes que BigQuery doit comparer. Appliquez le clustering sur vos colonnes unique_key quand vous utilisez merge.
La stratégie insert_overwrite remplace des partitions entières plutôt que de fusionner des lignes individuelles. Cela nécessite une configuration partition_by mais pas de unique_key. Pour les données de séries temporelles avec des enregistrements immuables, insert_overwrite est plus efficace que merge :
{{ config( materialized='incremental', incremental_strategy='insert_overwrite', partition_by={ 'field': 'event_date', 'data_type': 'date' }, cluster_by=['event_name', 'user_id']) }}La stratégie microbatch (introduite dans dbt 1.9) traite les lots temporels indépendamment avec des capacités intégrées de réessai et de remplissage historique. Sur BigQuery, elle utilise insert_overwrite sous le capot :
{{ config( materialized='incremental', incremental_strategy='microbatch', event_time='created_at', batch_size='day', partition_by={ 'field': 'created_at', 'data_type': 'timestamp', 'granularity': 'day' }) }}L’optimisation copy_partitions
Pour insert_overwrite avec le partitioning par temps d’ingestion, activer copy_partitions utilise l’API copy table de BigQuery au lieu des instructions MERGE. Cela élimine les coûts d’insertion et améliore les performances d’environ 40 % sur les grandes partitions :
{{ config( materialized='incremental', incremental_strategy='insert_overwrite', partition_by={ 'field': 'created_date', 'data_type': 'timestamp', 'granularity': 'day', 'time_ingestion_partitioning': true, 'copy_partitions': true }) }}Pièges critiques dbt
_dbt_max_partition est du SQL BigQuery, pas du Jinja. Utilisez-le directement dans votre SQL sans doubles accolades :
-- CorrectWHERE event_date >= _dbt_max_partition
-- FauxWHERE event_date >= {{ _dbt_max_partition }}Changer la colonne de partition entraîne une perte de données. Quand vous modifiez partition_by sur une table incrémentale existante, l’insert_overwrite de dbt supprime les partitions basées sur les valeurs de la nouvelle colonne — qui peuvent ne pas correspondre aux données existantes. Exécutez toujours --full-refresh quand vous modifiez la configuration de partition.
Les incompatibilités de types de données cassent le pruning silencieusement. Si votre colonne de partition est DATE mais que vous filtrez avec un littéral TIMESTAMP, le pruning ne se fera pas. Soyez explicite sur les types dans vos conditions de filtre.
Anti-patterns à éviter
Chacune de ces erreurs peut éliminer les bénéfices de performance que vous essayez d’atteindre.
Sur-partitioning
Symptôme : Des centaines ou milliers de partitions, chacune sous 1 Go.
Problème : BigQuery doit assembler les petites partitions au moment de la requête. Le overhead de métadonnées annule toute amélioration de vitesse.
Solution : Utilisez une granularité plus grossière (mensuelle au lieu de quotidienne) combinée au clustering pour un filtrage temporel plus fin.
Mauvais ordre des colonnes de clustering
Symptôme : Les requêtes filtrent sur la deuxième ou troisième colonne de clustering sans inclure la première.
Problème : Le clustering fonctionne hiérarchiquement. Sauter la première colonne signifie qu’aucun block pruning ne se produit.
Solution : Placez toujours la colonne la plus fréquemment filtrée en premier. Auditez vos patterns de requêtes réels — ne devinez pas.
Fonctions sur les colonnes de partition
Symptôme : Des filtres comme WHERE DATE(event_timestamp) = '2025-01-10' sur une table partitionnée par TIMESTAMP.
Problème : Envelopper la colonne de partition dans une fonction empêche le partition pruning.
Solution : Créez une colonne DATE dédiée pour le partitioning, ou assurez-vous que votre filtre correspond exactement au type de la colonne de partition.
Expressions de filtre dynamiques
Symptôme : Des filtres de partition qui dépendent de sous-requêtes ou de valeurs calculées à l’exécution.
Problème : BigQuery ne peut pas pruner les partitions quand la valeur du filtre n’est pas connue au moment de la planification de la requête.
Solution : Matérialisez les valeurs de filtre dans des CTEs ou des requêtes séparées. Pour les modèles incrémentaux, utilisez _dbt_max_partition que BigQuery peut évaluer.
require_partition_filter manquant
Symptôme : Des scans complets de table accidentels sur des tables de plusieurs téraoctets.
Problème : Une seule requête négligente sans filtre de date peut coûter des centaines de dollars.
Solution : Activez toujours require_partition_filter=true sur les grandes tables de production. Cela force les requêtes à inclure un filtre de partition, empêchant les erreurs coûteuses.
Comparer les colonnes clusterisées à d’autres colonnes
Symptôme : Des filtres comme WHERE customer_id = order_id.
Problème : Le block pruning nécessite une comparaison avec des valeurs constantes. Les comparaisons colonne-à-colonne scannent tous les blocs.
Solution : Si vous avez besoin de ce type de filtre, envisagez de restructurer votre modèle de données.
Valider vos choix
N’optimisez pas à l’aveugle. Mesurez avant et après.
Avant déploiement
Vérifiez les statistiques actuelles de la table en utilisant INFORMATION_SCHEMA :
SELECT table_name, total_rows, total_logical_bytes / POW(1024, 3) AS size_gb, ARRAY_TO_STRING(clustering_columns, ', ') AS clusteringFROM `project.dataset.INFORMATION_SCHEMA.TABLE_STORAGE`WHERE table_name = 'your_table'Utilisez le Recommender de BigQuery (l’icône ampoule dans la console) pour voir les suggestions ML pour le partitioning et le clustering basées sur vos patterns de requêtes réels.
Prévisualisez vos requêtes en dry-run pour voir les octets scannés avant exécution :
-- Dans la console BigQuery, activez le mode "Dry run"-- Ou utilisez la ligne de commande bq :bq query --dry_run --use_legacy_sql=false 'SELECT ...'Après déploiement
Surveillez les octets scannés et le slot time dans l’historique des requêtes BigQuery. Comparez avant/après pour des requêtes représentatives.
Vérifiez les détails d’exécution des requêtes pour l’efficacité du partition pruning :
SELECT total_bytes_processed, total_bytes_billed, total_slot_msFROM `region-us`.INFORMATION_SCHEMA.JOBSWHERE job_id = 'your-job-id'Surveillez les avertissements de filtre de partition dans vos logs dbt. Ils indiquent les requêtes qui n’ont pas pu pruner les partitions.
Conclusion
La décision partitioning vs. clustering se résume à quelques principes clés.
- Partitionnez quand vous avez besoin de prévisibilité des coûts et de gestion du cycle de vie sur une seule colonne temporelle
- Clusterez quand vous filtrez sur plusieurs colonnes ou des valeurs haute cardinalité
- Combinez les deux pour les grandes tables avec des patterns de requêtes multidimensionnels
Les seuils de taille comptent : les bénéfices du clustering commencent autour de 64 Mo mais deviennent significatifs à partir de 1 Go+, tandis que les partitions devraient atteindre en moyenne au moins 10 Go chacune.
Pour les utilisateurs dbt, faites correspondre votre stratégie incrémentale à votre pattern de données. Insert_overwrite avec copy_partitions fournit le chemin le plus efficace pour les données de séries temporelles. Merge bénéficie du clustering sur les colonnes de jointure.
Plus important : mesurez vos patterns de requêtes réels avant d’optimiser. Le Recommender de BigQuery, les vues INFORMATION_SCHEMA et les détails d’exécution des requêtes vous donnent les données pour valider vos choix. Commencez avec la configuration suggérée par vos patterns de requêtes dominants, surveillez les octets scannés et le slot time, et itérez.