Relations de la spine référencées par clé naturelle (releve_id) : normaliser chronologie_releves, surrogate/FK différés
Statut
accepted — précise et prolonge ADR-0041 : celui-ci a posé la spine CTI (épine + situation forward-fillée + relations rattachées par source) mais a laissé implicite quelle colonne va dans la spine et comment une relation s'y rattache physiquement. Réalise l'intention d'ADR-0029 (releves = modèle canonique, source de vérité unique) et d'ADR-0038 (releve_id = clé métier). Préserve ADR-0023 (périodisations séparées).
Contexte
ADR-0041 a fait de la Chronologie du contrat une spine CTI assemblée en dbt. Deux points restaient ouverts :
- (a) quelle colonne appartient à la spine, vs à une relation ?
- (b) comment la relation relevés s'y rattache physiquement ?
Aujourd'hui le mart chronologie_releves recopie le payload d'index (index_*_kwh,
nature_index, id_calendrier_distributeur) alors qu'il porte déjà releve_id — une
duplication qui contredit la source de vérité unique d'ADR-0029/0038.
Intuition de départ (Virgile) : poser une vraie FK interne / surrogate entier pour la perf de jointure (« sinon on pète l'opti »). C'est l'inconnue porteuse → spike.
Finding du spike (DuckDB 1.5.3 / dbt-duckdb 1.10.1, base dev = 441 867 relevés) :
DuckDB applique la FK mais seulement inline au CREATE TABLE (ALTER ADD FOREIGN
KEY non implémenté → post-hook mort) ; dbt-duckdb expose foreign_key = ENFORCED mais
uniquement via les model contracts (schéma typé colonne par colonne sur les deux
marts = taxe YAML réelle). La jointure INT64 vs VARCHAR(16) est 2,17× plus rapide
mais 4,8 ms contre 10,4 ms à l'échelle du parc → immatériel. ⟹ la perf ne justifie
ni surrogate ni FK ici ; le gain de la normalisation est de modèle (source de
vérité unique), pas de vitesse. La prémisse « vraie FK sinon on pète l'opti » est renversée.
Le diagramme chronologie-spine-modele
(source) argumente l'ensemble : substrat →
périodisations homogènes, règle d'intégration, dimension/fait/vue, et « pourquoi N frises ».
Décision
1. Règle d'intégration d'une colonne (rend explicite le CTI d'ADR-0041).
Une colonne entre dans la spine ⟺ c'est un attribut de situation : état contractuel à
sémantique forward-fill (step-function), nécessaire à des points qui ne sont pas son
événement-source (bornes FACTURATION, relevés périodiques). Tout le reste — le payload
propre à un fait (index, nature, calendrier ; demain le détail d'une affaire) — est une
relation, référencée par clé, pas inlinée. « Utile à ≥1 pipeline » n'est pas le
critère : les flux_* sont déjà filtrés aux colonnes utiles → ce critère inlinerait tout
→ STI (frame large nullable), écartée par 0041. Le partage par ≥2 pipelines est conséquence
de la nature « état », pas critère.
2. Normaliser chronologie_releves.
L'amincir à : identité + situation + releve_id (référence) + attributs de slot
calculés (ordre_index, source, releve_manquant) ; retirer le payload d'index
recopié (index_*_kwh, nature_index, id_calendrier_distributeur). Une vue dbt
(materialized: view) en star-join chronologie_releves ⨝ releves ON releve_id
ré-attache les index → releves = source de vérité unique. Le cœur (pipeline_energie)
lit la vue ; son contrat ChronologieReleves est inchangé.
Nuance load-bearing : la situation (FTA / niveau / RSC) reste dans le mart mince — sa
provenance est native C15 OU ffill-spine (≠ releves[releve_id], nul pour les
périodiques) ; seul le payload d'index pur (fonction 1:1 de releve_id) migre vers la
vue.
3. Clés.
releve_id (clé métier, ADR-0038) est la référence interne et la clé portable
(Odoo, rejouabilité inter-instances). Le surrogate entier + FK enforced sont différés —
à rouvrir seulement si le parc ×100, ou si l'intégrité enforced est voulue pour
elle-même (la taxe contracts + le gain perf nul ne le justifient pas aujourd'hui).
4. Nommage.
« Chronologie du périmètre » = le concept parapluie (spine + ses relations ; du
contrat / du point = des grains de coupe, pas des marts). On garde le mart
spine_contrat (son grain de forward-fill est le contrat/RSC). On dés-abrège les
loaders : spine() → spine_contrat(), chronologie() → chronologie_releves().
Alternatives écartées
- Surrogate entier + FK enforced maintenant — perf immatérielle à 442k lignes, FK seulement via la taxe contracts (schéma typé sur 2 marts) ; rouvrir à ×100.
- Garder la recopie du payload d'index (statu quo) — contredit la source de vérité
unique 0029/0038 ; corrigeable à coût quasi nul (le
releve_idest déjà là). - Inliner les relevés comme lignes de spine, ou une base large avec colonnes FK au grain-fait — le fan-out avant/après (2 relevés par événement C15) veut la référence au grain slot (côté « plusieurs ») ; et l'appariement périodique↔borne est calculé (equi-join jour), pas une clé native de l'événement.
- Renommer le mart
spine_contrat→chronologie_*— « spine » nomme la forme (l'épine de colonnes communes), le grain est le contrat, « périmètre » est le concept parapluie (pas un mart). - STI (frame large nullable) — déjà écartée par ADR-0041 ; rappelée car « utile à ≥1 pipeline » y mène tout droit.
Conséquences
relevesdevient la source de vérité unique des valeurs d'index (fin de la duplication ; aligne 0029/0038).- Le patron de relation généralise : une nouvelle relation (affaires/jalons) s'attache pareil — référence par clé, payload dans sa table, projection = vue.
chronologie_relevesreste un mart matérialisé (l'appariement equi-join jour + prioritéQUALIFYC15>R64>R151 n'est pas trivial) mais plus mince ; l'énergie lit une vue (un join de plus, ~10 ms à 442k → négligeable).- La règle d'intégration devient le critère normatif cité dès qu'une colonne nouvelle se présente (relais : affaires, accise…).
- Périodisations séparées préservées (ADR-0023) : les frises abonnement / énergie
(et NC… demain) ne fusionnent pas — leurs jeux de coupes se croisent (
puissance∈ abo-seul,index/calendrier∈ énergie-seul,fta∈ commun, aucun n'emboîte l'autre) et l'énergie est contrainte par la donnée (pas d'index = incalculable). Impossible dans les deux sens, l'union aussi. Une spine, N frises. (Diagramme.) - Suites (issues) : (1) amincir
chronologie_releves+ vue + énergie lit la vue + tests de parité ; (2) nommage / diagramme / glossaire (rename loaders, « Chronologie du périmètre »). Le surrogate/FK ne fait pas d'issue — différé, tracé ici avec son déclencheur (×100 ou intégrité voulue).