Quand des enregistrements de plusieurs systèmes sources sont fusionnés en une entité unifiée — un modèle Customer 360, un catalogue produits venant de plusieurs fournisseurs, un grand livre de transactions consolidé — des données conflictuelles apparaissent inévitablement. Le titre de poste d’un contact pourrait être « VP Sales » dans Salesforce et « Vice President, Sales » dans HubSpot. Son email pourrait avoir été mis à jour dans le CRM mais pas dans la base de données produit. Son numéro de téléphone pourrait exister dans un système et être null dans un autre.
Une stratégie s’impose pour définir quel système remporte le conflit, et cette stratégie doit être explicite, cohérente et auditable. La résolution implicite des conflits — ce qui se trouve en premier dans une jointure — produit des résultats imprévisibles et une dégradation silencieuse de la qualité des données.
Trois patterns gèrent la grande majorité des cas.
Résolution basée sur la priorité
Sélectionner depuis une liste classée de sources. Le système le plus autoritaire gagne, quel que soit le moment de mise à jour des données.
COALESCE(crm.email, product.email, ga4.email) AS customer__email,COALESCE(crm.company_name, product.company_name) AS customer__company_name,COALESCE(crm.phone, product.phone) AS customer__phoneCOALESCE retourne la première valeur non-null. L’ordre des arguments définit la priorité : les données CRM l’emportent sur les données produit, qui l’emportent sur les données GA4.
Quand l’utiliser : quand un système est clairement le système de référence pour un champ donné. Le CRM est la source de vérité pour les coordonnées de contact. Le système de facturation est la source de vérité pour les informations de paiement. La base de données produit est la source de vérité pour l’utilisation des fonctionnalités. La priorité doit refléter le processus métier qui maintient chaque champ.
Forces : Simple. Facile à comprendre, à auditer et à déboguer. Quand une partie prenante demande « d’où vient l’email ? », la réponse tient en une ligne : « CRM d’abord, puis produit, puis GA4. »
Faiblesses : Un email CRM exact il y a deux ans l’emporte sur un email produit mis à jour hier. La priorité ignore la récence, ce qui peut poser problème pour les champs qui changent fréquemment.
Priorité par champ
L’ordre de priorité n’a pas à être le même pour chaque champ. En pratique, il devrait rarement l’être :
-- Le CRM fait autorité pour les coordonnéesCOALESCE(crm.email, product.email) AS customer__email,COALESCE(crm.company_name, product.company_name) AS customer__company_name,
-- Le produit fait autorité pour les données d'usageCOALESCE(product.last_login_at, crm.last_activity_at) AS customer__last_active_at,
-- La facturation fait autorité pour les données de paiementCOALESCE(billing.plan_name, product.plan_name) AS customer__plan_nameDocumentez ces priorités par champ dans la description YAML du modèle ou dans un bloc de docs dbt. Les futurs mainteneurs (y compris soi-même) doivent comprendre pourquoi le CRM gagne pour l’email mais le produit gagne pour les horodatages d’activité.
Résolution basée sur la récence
Prendre la valeur qui a été mise à jour le plus récemment, quelle que soit la source. L’hypothèse est que la mise à jour la plus récente d’un champ est la plus précise.
CASE WHEN crm.updated_at >= COALESCE(product.updated_at, TIMESTAMP('1970-01-01')) THEN crm.job_title ELSE product.job_titleEND AS customer__job_titleQuand l’utiliser : pour les champs où la mise à jour la plus récente est probablement la plus précise — titre de poste, numéro de téléphone, adresse, nom d’entreprise. Ces données évoluent, et le système qui a capturé le changement le plus récemment a probablement raison.
Forces : S’adapte automatiquement. Si un contact met à jour son titre de poste dans les paramètres de profil du produit, il n’est pas nécessaire d’une synchronisation CRM pour récupérer le changement. La valeur la plus récente gagne.
Faiblesses : Nécessite des horodatages updated_at de chaque source, et ces horodatages doivent être fiables. Si un système utilise la date de création de l’enregistrement au lieu de la date de dernière modification, la comparaison de récence n’a aucun sens. Plus complexe aussi que COALESCE, surtout avec trois sources ou plus.
Passage à l’échelle avec plusieurs sources
Pour plus de deux sources, un pattern utilisant ARRAY_AGG avec tri évite les instructions CASE profondément imbriquées :
( SELECT value FROM UNNEST([ STRUCT(crm.job_title AS value, crm.updated_at AS updated_at), STRUCT(product.job_title AS value, product.updated_at AS updated_at), STRUCT(billing.job_title AS value, billing.updated_at AS updated_at) ]) WHERE value IS NOT NULL ORDER BY updated_at DESC LIMIT 1) AS customer__job_titleC’est une syntaxe spécifique à BigQuery. Sur Snowflake, il faudrait utiliser un lateral flatten ou un CTE avec ROW_NUMBER. Le principe est identique : collecter toutes les valeurs non-null avec leurs horodatages, sélectionner la plus récente.
Champs spécifiques à la source
Contourner entièrement le conflit. Au lieu de choisir un email, les conserver tous :
crm.email AS customer__crm_email,product.email AS customer__product_email,ga4.email AS customer__ga4_emailQuand l’utiliser : quand les consommateurs en aval ont besoin de connaître la provenance d’une valeur. Un reverse ETL vers le CRM doit utiliser l’email CRM, pas un email produit qui pourrait différer. Une plateforme d’email marketing pourrait vouloir essayer toutes les adresses connues. Un tableau de bord qualité des données pourrait signaler les enregistrements où les emails divergent.
Forces : Pas de perte d’information. La valeur de chaque source est préservée. Les consommateurs font leur propre choix sur lequel utiliser.
Faiblesses : Élargit la table. Trois colonnes email au lieu d’une. Pour un Customer 360 avec cinq systèmes sources et vingt champs conflictuels, cela produit cent colonnes. Reporte également la décision de résolution au consommateur, ce qui peut mener à une logique en aval incohérente si différentes équipes choisissent des colonnes différentes.
Un hybride pragmatique
En pratique, la plupart des équipes combinent les patterns. Utiliser un champ résolu principal (basé sur la priorité ou la récence) aux côtés des champs spécifiques à la source pour la provenance :
-- Le champ résolu pour un usage généralCOALESCE(crm.email, product.email) AS customer__email,
-- Champs sources pour la provenance quand nécessairecrm.email AS customer__crm_email,product.email AS customer__product_emailCela donne aux consommateurs en aval un défaut sensé tout en préservant la capacité à surcharger quand des exigences spécifiques l’imposent.
Choisir un pattern par type de champ
| Type de champ | Pattern recommandé | Rationale |
|---|---|---|
| Email, téléphone | Basé sur la priorité | Le système de référence est généralement clair |
| Titre, entreprise | Basé sur la récence | Évolue dans le temps ; la valeur la plus récente est généralement correcte |
| Adresse | Basé sur la récence | Identique au titre |
| Plan/abonnement | Basé sur la priorité (système de facturation) | Le système financier fait autorité |
| Date de dernière activité | Basé sur la récence (la plus récente entre les sources) | Utiliser GREATEST() sur tous les horodatages |
| Métriques d’usage | Spécifiques à la source | Différents systèmes mesurent des choses différentes |
| Données financières | Basé sur la priorité (système de facturation) | Un seul système financier de référence |
Clés de substitution pour les entités multi-sources
Lors de la construction d’entités unifiées depuis plusieurs sources, la clé de substitution doit encoder à la fois le système source et l’ID source :
{{ dbt_utils.generate_surrogate_key(['source_system', 'source_id']) }} AS customer__surrogate_keyCela évite les collisions entre systèmes qui pourraient réutiliser les mêmes IDs internes (les deux systèmes pourraient avoir un contact avec l’ID 12345). Résolvez les clés de substitution vers un customer_id canonique via le pont d’identité.
Documentation et tests
La logique de résolution de conflits est facile à bien faire initialement et facile à casser lors des refactorisations. Protégez-la avec :
- Des descriptions YAML sur chaque champ résolu qui documentent l’ordre de priorité ou la stratégie de résolution
- Des tests dbt qui vérifient que le champ résolu n’est jamais null quand au moins une source a une valeur (détecte les bugs d’ordre COALESCE)
- Des contrôles qualité des données qui signalent les enregistrements où les sources sont en désaccord, afin de surveiller la fréquence réelle des conflits et si la stratégie de résolution produit des résultats sensés
La stratégie de résolution doit être une décision consciente et documentée — pas un artefact de l’ordre des jointures.