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) :

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é :

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é :

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 :

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

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.

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).
OpenCaps / Greenstratai — Document de travail — v4