<!-- gdoc: PENDING -->
# Plateforme de données — Architecture trois étages

**Victor Lequay — Mars 2026 — Document de travail, v3 (post revue externe)**

---

## Le problème

Notre stack actuelle (datapipe + dataprovider + hemis) a été conçue pour une vision de la donnée qui ne correspond plus à nos besoins. L'audit du code existant révèle des problèmes structurels majeurs.

### Problèmes identifiés dans le code

**1. Aucune préservation des données brutes**
Quand Enedis envoie des index demi-horaires en Wh, `DataConverter` convertit immédiatement en kWh et mappe vers `FIELD_NAME.DIFF`. Le XML SOAP original est jeté. Si la logique de conversion a un bug ou si les hypothèses changent, impossible de retraiter depuis la source. L'API Enedis a une fenêtre glissante de 24 mois — au-delà, la donnée source est perdue.

**2. La distinction FACT/SEN n'est pas respectée**
CT-Injector écrit les relevés Enedis bruts avec `nature=FACT`, alors que FACT signifie "valeur calculée/agrégée". Hemis écrit les mesures capteurs (`SEN`) ET les agrégats (`FACT`). Rien n'empêche d'écrire du brut comme du calculé. La distinction n'existe que par convention.

**3. Deux versions de DataProvider en production**
Hemis utilise `1.2.0-SNAPSHOT` (Java 8), datapipe utilise `2.0.0-SNAPSHOT` (Java 21). Les ontologies, politiques de rétention et signatures d'API peuvent diverger. Aucun test de compatibilité.

**4. Pertes de données silencieuses**
`DataProvider.persistData()` attrape `TSDBException` et `InvalidParameterException` en interne sans les relancer. Datapipe retourne HTTP 200. CT-Injector avance `lastFetchTime`. Aucune donnée écrite, aucune anomalie envoyée. Mécanisme de perte latent sans chemin de détection.

**5. Double ingestion Enedis**
Hemis (`EnedisComManager`) et CT-Injector peuvent ingérer les données Enedis pour le même bâtiment. Les deux écrivent dans `nature=FACT, group=CPOW_DIFF` mais avec des `dataSourceId` différents. Sans filtre `dsid`, les requêtes retournent les deux.

**6. Trois instances datapipe = trois systèmes différents**
`datapipe.ubiant.io` (DATASTORE), `datapipe-rt.ubiant.io` et `datapipe-flexompremium` (HEMIS/BRIDGES) ont des ontologies, des rétentions, et des comportements différents. Le routage dépend de `custom_datapipe_url` dans metainfodb, vérifié seulement par ct-injector.

**7. Le dispatch crée des données dérivées indistinguables des mesures**
Quand CT-Injector dispatche l'énergie d'un parent vers ses enfants au prorata de surface, les données enfants sont stockées avec les mêmes tags (`FACT/CPOW_DIFF`) que les mesures réelles. Aucun tag n'indique qu'il s'agit d'estimations.

**8. Qualité fragmentée**
Le champ `RELIABILITY` dans InfluxDB 1.8 et les anomalies dans GEDB (InfluxDB 2.4) sont deux signaux de qualité complètement déconnectés. Un consommateur ne peut pas corréler les deux.

**9. La transformation `manageCpid()` est irréversible**
En profil DATASTORE, `CPOW + cpid=DIFF` est réécrit en `CPOW_DIFF + cpid=null`. L'information cpid originale est perdue en base.

---

## Architecture proposée : trois dataspaces

Inspirée de l'architecture medallion (bronze/silver/gold) mais adaptée à notre domaine.

**Principe directeur** : logique forte > distribution. Les trois couches sont des **séparations logiques** (schémas dans une même base), pas nécessairement trois services distincts dès le départ. Les microservices viendront quand le scaling l'imposera.

### Vue d'ensemble

```
Fournisseurs          BRUT              CORRIGÉ            ENRICHI
                    (Bronze)           (Silver)            (Gold)
                       │                  │                  │
Enedis ──────┐    ┌────┴─────┐     ┌─────┴──────┐    ┌─────┴──────┐
GRDF ────────┤    │ Ingestion│     │Harmonisation│    │ Synthèse   │
CPCU ────────┤───▶│ Archivage│────▶│ Correction  │───▶│ Agrégation │
Fraîcheur ───┤    │ Contrôle │     │ Fiabilité   │    │ Prédiction │
Capteurs ────┘    │ Alerte   │     │ Gap filling │    │ KPIs       │
                  └──────────┘     └────────────┘    └────────────┘
                       │                  │                  │
                  Donnée brute      Donnée fiable      Donnée métier
                  (preuve légale)   (exploitable)      (aide décision)
```

### 1. BRUT (Raw Dataspace)

**Rôle** : Ingérer, archiver, ne pas transformer. Source de vérité légale.

**Double stockage** : un TSDB seul ne suffit pas pour la preuve légale. Le Raw est composé de :

1. **Object storage (S3/MinIO, mode WORM)** — payloads originaux (XML SOAP Enedis, JSON GRDF, réponses CPCU) archivés tels quels. Immuable. Preuve légale.
2. **Base temps (TimescaleDB ou autre)** — valeurs extraites des payloads, indexées pour requêtage rapide. Liées au payload source.

| Aspect | Détail |
|--------|--------|
| **Ce qui entre** | Relevés bruts TELS QUELS : index Wh (Enedis), m³ (GRDF), MWh (CPCU), mesures capteurs, métadonnées fournisseur |
| **Payload original** | Archivé en object storage (S3/MinIO WORM). Référencé par `payload_ref` dans la base temps. |
| **Contrôles** | Format uniquement : timestamp valide, valeur numérique, identifiant compteur connu. Aucune conversion. |
| **Stockage** | Append-only, immuable. Pas de mise à jour, pas de suppression (sauf RGPD). |
| **Rétention** | 5+ ans — contrainte légale pour les données de facturation |
| **Alertes** | Absence de données (compteur silencieux), doublon, format invalide |

**Modèle de données enrichi** (vs le modèle initial trop pauvre) :

```sql
CREATE TABLE raw.events (
    id                  BIGSERIAL,
    received_at         TIMESTAMPTZ NOT NULL DEFAULT now(),  -- quand on l'a reçu
    source_timestamp    TIMESTAMPTZ NOT NULL,                -- timestamp du fournisseur
    source_interval_start TIMESTAMPTZ,                       -- début de la période mesurée
    source_interval_end   TIMESTAMPTZ,                       -- fin de la période mesurée
    meter_id            TEXT NOT NULL,                        -- PRM, PCE, n° client
    provider            TEXT NOT NULL,                        -- enedis, grdf, cpcu, ...
    entity_id           TEXT,                                 -- ID bâtiment/entité
    value               DOUBLE PRECISION NOT NULL,           -- valeur brute
    unit                TEXT NOT NULL,                        -- Wh, m3, MWh, °C
    quality_code        TEXT,                                 -- code qualité fournisseur (Enedis: N, Tc, Iv, Ec)
    source_timezone     TEXT DEFAULT 'UTC',                   -- fuseau du fournisseur
    payload_ref         TEXT,                                 -- lien vers payload S3/MinIO
    payload_hash        TEXT,                                 -- SHA-256 du payload original
    parser_version      TEXT,                                 -- version du parseur utilisé
    source_event_id     TEXT,                                 -- ID unique côté fournisseur si disponible

    PRIMARY KEY (source_timestamp, meter_id, provider)
);

SELECT create_hypertable('raw.events', 'source_timestamp');
```

### 2. CORRIGÉ (Corrected Dataspace)

**Rôle** : Nettoyer, harmoniser, qualifier. Produire un flux exploitable avec garanties de qualité.

**Principe clé : append-only versionné, pas mutable.** On ne modifie jamais une valeur corrigée — on la remplace par une nouvelle version. Chaque valeur pointe vers sa source brute et la règle qui l'a produite.

| Aspect | Détail |
|--------|--------|
| **Harmonisation temporelle** | Rééchantillonnage sur pas réguliers (horaire, journalier). Stockage UTC, métadonnée du fuseau source. |
| **Conversion d'unités** | Wh → kWh, index → delta. Gaz : m³ → kWh via PCS versionné (table dédiée par zone/période/compteur). |
| **Dédoublonnage** | Détection et suppression des relevés reçus en double |
| **Détection d'aberrations** | Valeurs hors plage physique. Règles paramétrables par type de compteur/usage. |
| **Gap filling** | Interpolation linéaire pour trous courts (<24h). Au-delà : point marqué "manquant". |
| **Score de fiabilité** | Chaque point : 1.0 = mesuré, 0.8 = interpolé court, 0.5 = estimé, 0.0 = manquant. |
| **Traçabilité** | Chaque valeur pointe vers `raw_event_id` + `rule_version`. |
| **Versionnement** | Pas d'UPDATE. Nouvelle version avec `supersedes_id` + `is_current`. Permet le rejeu. |
| **Origine** | Tag explicite : `measured`, `dispatched`, `interpolated`, `estimated`. |

**Modèle versionné** :

```sql
CREATE TABLE corrected.values (
    id                  BIGSERIAL,
    ts                  TIMESTAMPTZ NOT NULL,                -- timestamp harmonisé (UTC)
    meter_id            TEXT NOT NULL,
    provider            TEXT NOT NULL,
    entity_id           TEXT,
    value               DOUBLE PRECISION NOT NULL,           -- valeur corrigée (kWh, kWh, °C)
    unit                TEXT NOT NULL,
    reliability         DOUBLE PRECISION NOT NULL DEFAULT 1.0,
    origin              TEXT NOT NULL DEFAULT 'measured',     -- measured, dispatched, interpolated, estimated
    raw_event_id        BIGINT,                              -- FK vers raw.events
    rule_version        TEXT,                                 -- version de la règle de correction
    supersedes_id       BIGINT,                              -- version précédente (NULL si première)
    is_current          BOOLEAN NOT NULL DEFAULT true,
    produced_at         TIMESTAMPTZ NOT NULL DEFAULT now(),

    PRIMARY KEY (ts, meter_id, provider, produced_at)
);

SELECT create_hypertable('corrected.values', 'ts');
CREATE INDEX idx_corrected_current ON corrected.values (meter_id, provider, ts) WHERE is_current = true;
```

**Conversion gaz — PCS versionné** :

```sql
CREATE TABLE corrected.pcs_coefficients (
    id              SERIAL PRIMARY KEY,
    zone            TEXT NOT NULL,            -- zone géographique
    meter_id        TEXT,                     -- NULL = zone entière, sinon compteur spécifique
    valid_from      DATE NOT NULL,
    valid_to        DATE,
    pcs_kwh_per_m3  DOUBLE PRECISION NOT NULL, -- coefficient de conversion
    source          TEXT,                     -- GRDF bulletin, contrat, etc.
    UNIQUE(zone, meter_id, valid_from)
);
```

### 3. ENRICHI (Enriched Dataspace)

**Rôle** : Calculer, agréger, prédire. Indicateurs métier.

**Principe clé** : utiliser au maximum les **continuous aggregates de TimescaleDB** pour les agrégations standard (SUM, AVG, MIN, MAX par jour/mois/an). Le service d'enrichissement ne gère que la logique métier complexe.

| Via continuous aggregates (SQL natif) | Via service dédié |
|---------------------------------------|-------------------|
| Agrégations temporelles (jour, mois, an) | Synthèse parent/enfant (dispatch) |
| Min / max / moyenne | DJU (degrés-jour, croisement météo) |
| Sommes par groupe/nature | Prédictions (ML) |
| | Anomalies fines (corrélation multi-fluides) |
| | KPIs complexes (kWh/m²/an, classe DPE) |

---

## Choix technologiques — Discussion

### Rappel : ACID

ACID (Atomicity, Consistency, Isolation, Durability) garantit qu'une écriture en base est soit complètement réussie, soit complètement annulée. PostgreSQL/TimescaleDB offre ces garanties. InfluxDB et ClickHouse non — une écriture peut partiellement réussir sans transaction. Pour des données append-only (Raw), c'est acceptable (on peut ré-écrire). Pour le Corrigé versionné, ACID garantit qu'un remplacement de version est atomique.

### Candidats

Sept moteurs évalués. Prometheus inclus pour référence.

#### Comparaison détaillée

**Architecture et modèle de données**

| | InfluxDB 2.x | InfluxDB 3.x | TimescaleDB | ClickHouse | QuestDB | VictoriaMetrics | GreptimeDB |
|-|-------------|-------------|-------------|------------|---------|-----------------|------------|
| **Écrit en** | Go | Rust | C (PostgreSQL) | C++ | Java/C++/Rust | Go | Rust |
| **Modèle** | Measurement + tags + fields | Measurement + tags + fields | Tables relationnelles | Tables colonnes (MergeTree) | Tables colonnes | Metric name + labels | Tables (wide events) |
| **Types de valeurs** | float, int, string, bool | float, int, string, bool | Tous types SQL | Tous types SQL | Tous types SQL | **float64 uniquement** | Tous types SQL |
| **Métadonnées** | Tags (string indexé) | Tags (string indexé) | Colonnes SQL | Colonnes SQL | Colonnes SQL | Labels (string) | Colonnes SQL + labels |
| **Schéma** | Schemaless | Schemaless | Strict (DDL) | Semi-strict | Semi-strict | Schemaless | Semi-strict |

**Langage de requête**

| | InfluxDB 2.x | InfluxDB 3.x | TimescaleDB | ClickHouse | QuestDB | VictoriaMetrics | GreptimeDB |
|-|-------------|-------------|-------------|------------|---------|-----------------|------------|
| **Langage** | Flux QL | SQL + InfluxQL + Flux | SQL standard | SQL standard | SQL étendu | MetricsQL (superset PromQL) | **SQL + PromQL** |
| **JOINs** | Non | Oui (SQL) | Oui | Oui | **Oui (incluant ASOF JOIN)** | Non | **Oui (INNER, LEFT, RIGHT, FULL)** |
| **Window functions** | Limité | Oui | Oui | Oui | Oui | Non | Oui |
| **Sous-requêtes** | Limité | Oui | Oui | Oui | Oui | Limité | Oui |
| **Continuous aggregates** | CQ / Tasks | Tasks | **Natif (materialized)** | Materialized views | Non natif | Non | **Oui (Flow engine)** |
| **PostgreSQL wire protocol** | Non | Non | **Natif** | Non | **Oui** | Non | **Oui** |

**Performance (benchmarks TSBS IoT + benchmarks éditeurs, 2024-2025)**

| Métrique | InfluxDB 2.x | InfluxDB 3.x | TimescaleDB | ClickHouse | QuestDB | VictoriaMetrics | GreptimeDB |
|----------|-------------|-------------|-------------|------------|---------|-----------------|------------|
| **Ingestion (pts/s)** | ~330K | ~3.3M (annoncé 10x) | ~1.3M (batch) | ~3M | ~960K–12M | ~2.2M | ~700K (edge ARM), ? (server) |
| **vs InfluxDB 2.x** | baseline | ~10x | ~4x | ~9x | ~12-36x | ~6.7x | ? |
| **Requêtes simples** | Bon | Très bon (sub-10ms) | Bon | Bon | Excellent (lastpoint) | Bon | ? |
| **Requêtes complexes** | Flux crash parfois | Bon | 1.75–8.6x vs Influx 2 | Le plus rapide | 43–418x vs Influx 2 | Bon (MetricsQL) | ? (SQL complet) |
| **RAM (ingestion)** | ~20.5 GB | ? | ~10 GB | ~10 GB | ? | ~6 GB | <150 MB (edge) |

*Attention : les benchmarks éditeurs sont biaisés. Les benchmarks TSBS sont plus neutres mais ne couvrent pas tous les candidats.*

**Stockage et compression**

| Métrique | InfluxDB 2.x | InfluxDB 3.x | TimescaleDB | ClickHouse | QuestDB | VictoriaMetrics | GreptimeDB |
|----------|-------------|-------------|-------------|------------|---------|-----------------|------------|
| **Compression** | ~3:1 | ~10:1 (Parquet) | 3:1–8:1 (natif), 10-20:1 (policies) | 10:1–30:1 (LZ4/ZSTD) | Bonne (exact ?) | **~70x vs TimescaleDB** | Bonne (Parquet/S3, exact ?) |
| **Octets/point** | ~6-8 | ~1-2 (Parquet) | ~29 | ~1-3 | ~4-6 | ~0.4-1 | ? |
| **Disque relatif** | 1x (référence) | ~0.3x | 4-12x | 0.3-0.5x | ~1x | **0.15-0.3x** | ? (S3 natif) |

*VictoriaMetrics affiche ~0.4 octet/point. Ces chiffres viennent de VM — à vérifier sur nos données.*

**Garanties et fonctionnalités**

| | InfluxDB 2.x | InfluxDB 3.x | TimescaleDB | ClickHouse | QuestDB | VictoriaMetrics | GreptimeDB |
|-|-------------|-------------|-------------|------------|---------|-----------------|------------|
| **ACID** | Non | Non | **Oui** | Partiel (INSERT atomique) | Non | Non | Non |
| **Immuabilité** | Non garanti | Parquet = immuable | Contraintes SQL | **Append-only natif** | **Append-only natif** | **Append-only natif** | Append-only (S3/Parquet) |
| **Updates** | Oui (écrase) | ? | **Oui (SQL UPDATE)** | Limité (expérimental) | Non | Non | ? |
| **Deletes** | Oui | Oui | **Oui (SQL DELETE)** | Bulk seulement | Range seulement | Range seulement | ? |
| **Multi-tenancy** | Orgs/buckets | Databases | Schemas/databases | Databases | Tables | Labels | Databases |
| **Haute dispo** | Enterprise | Enterprise | Réplication PG | Cluster natif | Enterprise | Cluster natif | **Natif (cloud-native)** |
| **Rétention configurable** | Oui (par bucket) | Oui | Oui (drop_chunks) | Oui (TTL) | Oui | Oui | Oui |

**Opérationnel**

| | InfluxDB 2.x | InfluxDB 3.x | TimescaleDB | ClickHouse | QuestDB | VictoriaMetrics | GreptimeDB |
|-|-------------|-------------|-------------|------------|---------|-----------------|------------|
| **Déploiement** | Binaire unique | Binaire unique | Extension PG | Cluster (complexe) | Binaire unique | Binaire unique | Binaire unique ou cluster |
| **Protocoles ingestion** | Line Protocol | Line Protocol | SQL INSERT | Natif, HTTP | **ILP + PG wire** | **ILP, Prometheus, Graphite, CSV** | **ILP, Prometheus, OTLP, PG wire** |
| **Licence** | MIT (v2 OSS) | Apache 2.0 (Core) | Apache 2.0 | Apache 2.0 | Apache 2.0 | Apache 2.0 | Apache 2.0 |
| **Maturité** | ~10 ans | ~2 ans (Rust rewrite) | ~8 ans (PG: 35 ans) | ~9 ans | ~5 ans | ~6 ans | **~3 ans (v1.0 récent)** |
| **Risque éditeur** | Pivot v3, breaking | Stabilisation en cours | Timescale + PG communauté | Ex-Yandex, fondation, très actif | Startup (plus petit) | VM Inc (solide) | **Startup jeune (Greptime Inc)** |

### Notes sur les candidats

**InfluxDB 3.x** — réécriture Rust avec Apache Arrow/DataFusion/Parquet. Performances annoncées impressionnantes mais produit jeune (~2 ans), breaking changes vs v2. SQL ajouté mais Flux conservé (maintenance mode). Le format Parquet le rapproche de ClickHouse. Pas recommandé pour un déploiement critique aujourd'hui. À surveiller.

**Prometheus** — système de collecte et d'alerting avec stockage local limité (15j par défaut). Pour le stockage longue durée → backend externe (Thanos, Cortex, VictoriaMetrics). Pertinent comme couche de collecte (Prometheus → remote_write → VM) ou pour le monitoring de notre infra.

**QuestDB** — sous-estimé dans les versions précédentes. Supporte les JOIN (incluant ASOF JOIN, très utile pour le time-series), le PostgreSQL wire protocol, et l'InfluxDB Line Protocol. Candidat sérieux.

**ClickHouse** — la formulation "non ACID" est à nuancer. Les INSERT sont atomiques dans certains cas, transactions expérimentales existantes. Pas un moteur transactionnel complet.

**VictoriaMetrics / Prometheus — analyse approfondie**

VM et Prometheus sont souvent proposés pour le stockage time-series. Ils ont des arguments forts : communauté massive, performances éprouvées, compression record (VM), écosystème riche (Grafana, AlertManager), et nos données sont quasi exclusivement des floats. La question mérite une réponse précise.

**Ce qui fonctionne parfaitement** : relevés de compteurs (kWh, m³, °C) → float64. Métadonnées (meter_id, provider, entity_id) → labels string. Ingestion via ILP ou remote_write. Compression VM imbattable. Ops simple (binaire unique).

**Ce qui coince pour notre cas d'usage** :

1. **Pas de jointures relationnelles** — Le Corrigé a besoin de traçabilité : "cette valeur corrigée vient de l'événement brut X, via la règle Y, remplaçant la version Z." En MetricsQL, impossible d'exprimer un JOIN corrected↔raw. Il faudrait faire deux requêtes et joindre côté application — faisable mais fragile et lent. Si les audits de traçabilité sont rares (pas temps réel), c'est tolérable. Si c'est un besoin quotidien, non.

2. **Pas de corrections versionnées** — VM écrase un point au même timestamp pour la même métrique. Pas de `supersedes_id`, pas de `is_current`, pas d'historique de version. On perd la capacité de dire "la valeur était X, puis on l'a corrigée en Y le 15 mars avec la règle v2." Encoder la version dans les labels (`rule_version="v2"`) est possible mais devient ingérable.

3. **Sampling implicite dans les requêtes** — VM et Prometheus peuvent silencieusement sous-échantillonner les résultats quand il y a trop de points dans la plage demandée. Pour un dashboard de monitoring, c'est transparent. Pour des données de facturation où chaque kWh compte, une approximation silencieuse est inacceptable. Contrôlable via le paramètre `step`, mais il faut savoir que ça existe.

4. **Pas de continuous aggregates** — TimescaleDB maintient automatiquement des vues matérialisées incrémentales (agrégations jour/mois/an mises à jour à chaque insertion). VM a du downsampling (Enterprise) et des recording rules, mais moins intégrés et moins flexibles que des agrégats SQL.

5. **Pas de transaction outbox** — Le pipeline nécessite "écrire les données brutes ET marquer pour traitement correctif dans une seule opération atomique." PostgreSQL le fait trivialement (même transaction). VM n'a aucun concept de transaction.

**Conclusion** : VM/Prometheus est le meilleur choix si on ne fait que du monitoring (stocker des métriques, afficher des dashboards, alerter sur des seuils). Notre cas a trois besoins que le monitoring ne couvre pas :
- Traçabilité relationnelle (raw → corrected)
- Corrections versionnées (historique, rejeu)
- Résultats de requête exacts (pas de sampling implicite sur données facturables)

**Si on accepte de renoncer à ces trois besoins**, VM est le meilleur choix. Si on les garde, il faut SQL pour au moins le Corrigé/Enrichi. VM reste un excellent candidat pour le Raw seul (option E).

**GreptimeDB** — candidat récent (v1.0 2026, Rust, Apache 2.0) qui combine les avantages de VM et TimescaleDB : SQL complet avec JOINs, PromQL (~90% compatible), ILP natif, PostgreSQL wire protocol, continuous aggregates (Flow engine), stockage S3 natif (compute-storage separation), multi-types (pas limité à float64), edge-compatible (700K pts/s sur ARM, <150MB RAM).

C'est le seul candidat qui offre **SQL + PromQL + ILP + JOINs + continuous aggregates + S3 natif** dans un seul moteur. En théorie, il pourrait couvrir les trois étages sans compromis majeur — le SQL avec JOINs résout le problème de traçabilité du Corrigé, les continuous aggregates couvrent l'Enrichi, et l'ILP + S3 couvrent le Raw.

**Ce qui manque** : pas d'ACID (même limitation que VM/ClickHouse/QuestDB — les corrections versionnées ne sont pas atomiques), benchmarks indépendants quasi inexistants (produit trop jeune), et **risque éditeur significatif** (startup de ~3 ans, communauté petite). Beaucoup de "?" dans les tableaux — les performances serveur ne sont pas encore bien documentées hors du cas edge/IoT.

**Verdict** : le candidat le plus prometteur sur le papier. Si GreptimeDB mûrit et prouve ses performances à notre échelle, il pourrait devenir le "un seul moteur pour tout" qui rend l'option A (TimescaleDB pour tout) obsolète avec de meilleures performances. Aujourd'hui, trop jeune pour un pari en production. À évaluer via un POC si le timing le permet.

### Besoins par étage

| Étage | Besoins dominants | Meilleurs candidats |
|-------|-------------------|---------------------|
| **Raw** | Append-only, compression (5+ ans × scale), immuabilité, lectures simples + archivage payload | TimescaleDB + S3 (ACID + object store) ou ClickHouse/VM (compression) + S3 |
| **Corrigé** | ACID ou append-only versionné, SQL + jointures (traçabilité raw→corrected), continuous aggregates | **TimescaleDB** (seul candidat avec ACID + SQL + continuous aggregates) |
| **Enrichi** | Requêtes analytiques, agrégations, KPIs | **TimescaleDB** (continuous aggregates natifs) + service Go pour logique métier |

### Options

#### Option A : TimescaleDB pour tout (recommandée pour démarrer)

```
1 cluster PostgreSQL + TimescaleDB
├── schema raw      (hypertable, compression policies)
├── schema corrected (hypertable, versionné)
├── schema enriched  (continuous aggregates + tables)
└── + S3/MinIO pour les payloads bruts
```

| + | − |
|---|---|
| Un seul moteur | Compression Raw inférieure à VM/ClickHouse |
| SQL + jointures cross-schema | Ingestion plus lente à très haut volume |
| ACID, continuous aggregates | |
| Écosystème PostgreSQL | |
| Déjà utilisé (ct-injector-go, consent-manager) | |
| Compression policies atténuent le disque (10-20x sur données >7j) | |

#### Option B : InfluxDB 2.x (Raw) + TimescaleDB (Corrigé/Enrichi)

| + | − |
|---|---|
| Infra InfluxDB existante | Flux QL, avenir incertain |
| | Pas de jointures Raw↔Corrigé |
| | Deux moteurs |

#### Option C : ClickHouse (Raw) + TimescaleDB (Corrigé/Enrichi)

| + | − |
|---|---|
| Compression 10-30x, SQL, analytique | MergeTree complexe à opérer |
| Append-only natif | Deux moteurs |
| INSERT atomique | Courbe d'apprentissage |

#### Option D : QuestDB (Raw) + TimescaleDB (Corrigé/Enrichi)

| + | − |
|---|---|
| Ingestion record, SQL, ASOF JOIN | Startup (risque pérennité) |
| ILP + PG wire protocol | Écosystème plus petit |
| Simple à opérer | |

#### Option E : VictoriaMetrics (Raw) + TimescaleDB (Corrigé/Enrichi)

| + | − |
|---|---|
| Compression record (70x vs TimescaleDB) | MetricsQL, pas SQL |
| Go natif, ILP compatible | Float64 only, modèle metrics |
| Coût stockage minimal | Moins adapté pour données facturables strictes |
| Simple à opérer | |

#### Option F : GreptimeDB pour tout (à évaluer)

```
1 cluster GreptimeDB
├── database raw      (ILP ingestion, S3 backend)
├── database corrected (SQL + JOINs, Flow engine)
├── database enriched  (continuous aggregates)
└── + S3/MinIO pour les payloads bruts (en plus du stockage natif S3)
```

| + | − |
|---|---|
| SQL + PromQL + ILP + JOINs + continuous aggregates en un seul moteur | **Très jeune** (v1.0, ~3 ans, startup) |
| S3 natif (compute-storage separation, coût optimisé) | Benchmarks indépendants quasi inexistants |
| Types multiples (pas limité à float64) | Pas d'ACID (corrections versionnées non atomiques) |
| PostgreSQL wire protocol | Communauté petite |
| Edge-compatible (IoT, ARM) | Risque éditeur |

**Verdict** : le candidat le plus complet sur le papier — il résout les limitations de VM (pas de SQL/JOINs) sans la lourdeur de TimescaleDB (compression, S3 natif). Mais trop jeune pour un pari en production aujourd'hui. **Recommandation : POC** pour valider les performances réelles et la stabilité avant tout engagement.

### Licensing

Tous les candidats sont **open-source (Apache 2.0)** et gratuits en self-hosted pour le core. Les features entreprise (RBAC, TLS, downsampling avancé, support) sont payantes chez QuestDB, VictoriaMetrics et ClickHouse. TimescaleDB a une licence "Timescale License" (TSL) pour la Community Edition : gratuite en self-hosted, mais interdiction de la revendre comme service managé.

Le coût réel n'est pas la licence — c'est l'**infrastructure** (CPU, RAM, disque).

### Coût réel à notre échelle

Nous gérons **des dizaines de milliers de bâtiments**. Estimation pour 10 000 bâtiments, ~5 compteurs/bâtiment, relevés horaires, 5 ans de rétention sur le Raw :

- 50 000 compteurs × 8 760 pts/an × 5 ans = **~2.2 milliards de points**

| Moteur | Octets/point | Stockage Raw estimé | RAM ingestion | Coût disque SSD cloud (~10€/TB/mois) |
|--------|-------------|---------------------|---------------|---------------------------------------|
| **TimescaleDB** (sans compression) | ~29 | **~63 TB** | ~10 GB | ~630€/mois |
| **TimescaleDB** (avec compression) | ~3-5 | **~7-11 TB** | ~10 GB | ~70-110€/mois |
| **InfluxDB 2.x** | ~6-8 | **~13-18 TB** | ~20 GB | ~130-180€/mois |
| **ClickHouse** | ~1-3 | **~2-7 TB** | ~10 GB | ~20-70€/mois |
| **QuestDB** | ~4-6 | **~9-13 TB** | ? | ~90-130€/mois |
| **VictoriaMetrics** | ~0.4-1 | **~1-2 TB** | ~6 GB | **~10-20€/mois** |
| **GreptimeDB** | ? (S3 natif) | ? (S3 = ~$23/TB/mois) | <1 GB (edge) | ? |

*Ces chiffres concernent le Raw seul. Le Corrigé et l'Enrichi sont moins volumineux (données agrégées). Les octets/point sont des moyennes de benchmarks éditeurs — à valider sur nos données réelles.*

**À cette échelle, la compression est le facteur de coût dominant.** VictoriaMetrics stocke en ~2 TB ce que TimescaleDB sans compression stocke en 63 TB. Même avec les compression policies activées (7-11 TB), la différence reste 5-10x.

Et c'est pour 10 000 bâtiments. Si on passe à 30 000 ou 50 000, les chiffres se multiplient linéairement.

### Résumé des options

| Option | Raw | Corrigé/Enrichi | Ops | Coût stockage Raw (10K bât, 5 ans) | Meilleur pour |
|--------|-----|-----------------|-----|-------------------------------------|---------------|
| **A** | TimescaleDB | TimescaleDB | ★☆☆ | ~70-110€/mois (compressé) | Simplicité, ACID, un seul moteur |
| **B** | InfluxDB 2.x | TimescaleDB | ★★☆ | ~130-180€/mois | Transition minimale (court terme) |
| **C** | ClickHouse | TimescaleDB | ★★★ | ~20-70€/mois | Analytique lourde, bonne compression |
| **D** | QuestDB | TimescaleDB | ★★☆ | ~90-130€/mois | Ingestion record, SQL + JOINs |
| **E** | VictoriaMetrics | TimescaleDB | ★★☆ | **~10-20€/mois** | **Coût minimal, Go natif** |
| **F** | GreptimeDB | GreptimeDB | ★☆☆ | ? (S3 natif) | Futur potentiel — POC nécessaire |

### Recommandation

Avec des dizaines de milliers de bâtiments, **l'option A (TimescaleDB pour tout) reste viable** grâce aux compression policies, mais n'est plus l'évidence. Le Raw sera le plus gros poste de stockage sur 5+ ans, et TimescaleDB est le moteur le plus gourmand en disque.

**Deux approches :**

1. **Commencer simple (option A)** — 1 cluster TimescaleDB, compression policies activées, ~70-110€/mois de stockage Raw. On accepte le surcoût disque en échange de la simplicité opérationnelle (un seul moteur, SQL partout, ACID). Si le coût devient un problème, on migre le Raw vers un moteur plus compact.

2. **Optimiser dès le départ (option E ou C)** — Raw sur VictoriaMetrics ou ClickHouse (~10-70€/mois), Corrigé/Enrichi sur TimescaleDB. Plus complexe à opérer (deux moteurs), mais coût de stockage 5-10x inférieur sur le long terme.

Le choix dépend de la priorité : **simplicité ops vs coût stockage**. À 10 000 bâtiments, la différence est de ~50-100€/mois. À 50 000 bâtiments, elle passe à ~250-500€/mois. Ce n'est pas négligeable mais ce n'est pas non plus un deal-breaker.

**GreptimeDB (option F)** mériterait un POC : s'il tient ses promesses, il offre la simplicité d'un seul moteur (comme A) avec le stockage S3 natif (comme E/C). Mais trop jeune pour s'engager sans validation.

### Langage

**Go**. Cohérent avec metainfodb, gedb, ct-injector-go, consent-manager. Binaire statique, déploiement Nomad.

### Pipeline — pas de microservices day 1

```
1 service Go (ou jobs internes SQL/cron)
    │
    ├── Ingestion → raw.events + S3 payload
    ├── Correction (cron/trigger) → corrected.values
    └── Enrichissement → enriched.* (continuous aggregates + jobs)
    │
    └── 1 cluster PostgreSQL+TimescaleDB
```

**Pas de message broker au démarrage.** LISTEN/NOTIFY n'est pas un bus fiable (limite 8KB, notifications non garanties, drops sous charge). Pattern recommandé :
- **Table outbox transactionnelle** : l'ingestion écrit dans `raw.events` ET dans `raw.outbox` dans la même transaction
- **Consumer idempotent** : le correcteur poll l'outbox, traite, marque comme fait
- **NOTIFY = simple wake-up** (optionnel) pour réduire la latence du polling

Référence : [Transactional Outbox Pattern](https://microservices.io/patterns/data/transactional-outbox.html)

Les services séparés viendront **quand le scaling l'imposera** — pas avant.

---

## Ontologie

Aujourd'hui, l'ontologie est codée en dur dans `DataSourceTypeOntology.java` (~5000 lignes). C'est le cœur du couplage.

**Proposition** : externaliser l'ontologie dans une configuration déclarative (YAML ou table PostgreSQL) chargée par chaque étage.

```yaml
data_types:
  electricity_consumption:
    raw_unit: Wh
    corrected_unit: kWh
    conversion: divide_1000
    groups: [CPOW_DIFF]
    retention_raw: 5y
    retention_corrected: infinite
    aberration_rules:
      min: 0
      max_delta_per_hour: 100000  # dépend du type de bâtiment
    gap_fill_max_hours: 24

  gas_consumption:
    raw_unit: m3
    corrected_unit: kWh
    conversion: pcs_table    # PCS versionné par zone/période, pas un simple multiply
    groups: [CGAS_DIFF]
    pcs_source: corrected.pcs_coefficients
    ...
```

**Note gaz** : la conversion m³ → kWh n'est PAS un simple `multiply_pcs`. Le PCS (Pouvoir Calorifique Supérieur) dépend de l'altitude, de la composition du gaz, et de la pression. Il est recalculé régulièrement par GRDF. La table `pcs_coefficients` doit être versionnée par zone, par période, et potentiellement par compteur.

---

## Ce qui change concrètement

| Aujourd'hui | Demain |
|------------|--------|
| InfluxDB 1.8 + Flux QL | TimescaleDB + SQL |
| Une base, tout mélangé | Trois schémas : raw, corrected, enriched |
| DataConverter jette le brut | Raw stocke la valeur originale + payload archivé en S3 |
| nature=FACT pour tout | Tags explicites : measured, dispatched, interpolated, estimated |
| RELIABILITY déconnecté de GEDB | Score de fiabilité rattaché à chaque point |
| Dispatch indistinguable des mesures | `origin = 'dispatched'` explicite |
| Corrections = UPDATE (écrasement) | Corrections = nouvelle version avec `supersedes_id` |
| SyntheticJob/PerMonthJob dans ct-injector | Continuous aggregates TimescaleDB + service enrichissement |
| CQ InfluxDB pour agrégations | Continuous aggregates TimescaleDB (natif, incrémental) |
| Ontologie codée en dur (Java 5000 lignes) | Configuration déclarative (YAML/DB) |
| dataprovider lib partagée Java 8/21 | Service(s) Go |
| Trois datapipe instances, routage fragile | Un pipeline unifié |
| PCS gaz = constante | PCS versionné par zone/période/compteur |

---

## Migration

Pas de big bang. Migration progressive en parallèle de l'existant.

| Phase | Contenu | Impact |
|-------|---------|--------|
| **1** | TimescaleDB + schéma raw + S3 payloads. ct-injector écrit en parallèle dans datapipe ET dans le Raw. | Aucun sur l'existant. |
| **2** | Pipeline Corrigé consomme le Raw (outbox). Validation croisée avec datapipe. | L'existant continue. |
| **3** | Continuous aggregates + service enrichissement. Remplace SyntheticJob/PerMonthJob. | Désactiver les jobs ct-injector. |
| **4** | Applications basculent vers Corrigé/Enrichi. | Reporter, MCP datapipe, chatbot. |
| **5** | Décommissionnement datapipe/dataprovider. | Extinction. |

---

## Questions ouvertes

1. **Volumétrie cible** : combien de bâtiments / compteurs à 1 an, 3 ans ? Centaines = option A. Milliers = envisager C ou E.
2. **Hemis** : ses données capteurs (SEN) vont dans le Raw ? C'est le flux le plus volumineux (sub-minute par capteur × milliers d'instances).
3. **TimescaleDB mono-instance ou séparées ?** Un cluster avec trois schémas, ou trois instances ?
4. **Ontologie** : YAML en config ou table en base (modifiable à chaud) ?
5. **Temps réel** : des cas d'usage sub-seconde ? Si oui, pipeline stream (broker nécessaire).
6. **Payloads bruts** : S3/MinIO déjà disponible ou à déployer ?
7. **PCS gaz** : source de données pour les coefficients versionnés ? Bulletin GRDF ?

---

## Historique du document

| Date | Changement |
|------|------------|
| 2026-03-24 | v1 — diagnostic + architecture trois étages + comparaison TSDB |
| 2026-03-25 | v2 — ajout QuestDB, VictoriaMetrics, InfluxDB 3.x, Prometheus. Comparaison détaillée 6 moteurs. |
| 2026-03-26 | v3 — intégration retour technique externe : pas de microservices day 1, Raw hybride (TSDB + object store), Corrigé versionné (pas mutable), outbox transactionnelle (pas LISTEN/NOTIFY), continuous aggregates pour Enrichi, PCS versionné, modèle Raw enrichi. Corrections QuestDB (JOINs), ClickHouse (ACID partiel), VictoriaMetrics (limites données facturables). |
