Skip to content
ClickHouse Docs
ClickHouse DocsClickHouse Docs

Runbook : schéma JSON

Vos données arrivent en JSON. ClickHouse vous propose plusieurs façons de les stocker, depuis des colonnes entièrement typées jusqu'à un String brut. Le bon choix dépend du degré de prévisibilité de votre schéma et de la nécessité d'effectuer des requêtes sur des champs individuels.

Portée : Cette page traite des choix de conception du schéma pour stocker des données JSON. Elle ne couvre pas les formats JSON d'entrée/sortie, les fonctions JSON, ni la syntaxe des requêtes. Pour plus d'informations sur le type de colonne JSON lui-même, consultez Use JSON where appropriate.

Prérequis : Connaissance de la création de tables ClickHouse, des bases de MergeTree et de la syntaxe des types de colonnes.

Décision rapide

  • Si chaque champ a un type connu et stable, et que le schéma évolue rarement Colonnes typées
  • Si la plupart des champs sont stables, mais qu’une partie est dynamique ou imprévisible Hybride (typé + JSON)
  • Si toute la structure est dynamique, avec des clés qui apparaissent et disparaissent d’un enregistrement à l’autre Colonne JSON native
  • Si les champs dynamiques sont des paires clé-valeur avec un type de valeur uniforme (par ex. des tags sous forme de chaînes ou des métriques numériques) Map plutôt que JSON
  • Si vous vous contentez de stocker et de récupérer le blob JSON, sans requêtes au niveau des champs Stockage opaque en String

Détails de l’approche

Colonnes typées

Quand l’utiliser : La structure JSON est entièrement connue dès la conception. Les champs et les types ne changent pas d’un enregistrement à l’autre. Même des structures imbriquées complexes (tableaux d’objets, maps imbriquées) peuvent être représentées avec les types Array, Tuple et Nested.

Compromis : Les changements de schéma nécessitent ALTER TABLE. Les champs inattendus sont silencieusement ignorés à l’insertion, sauf si le schéma est mis à jour.

Configuration, vérification et points d’attention

Configuration

CREATE TABLE events
(
    `timestamp` DateTime,
    `service`   LowCardinality(String),
    `level`     Enum8('DEBUG' = 1, 'INFO' = 2, 'WARN' = 3, 'ERROR' = 4),
    `message`   String,
    `host`      LowCardinality(String),
    `duration_ms` UInt32
)
ENGINE = MergeTree
ORDER BY (service, timestamp)

Vérification

-- Vérifier que les types de colonnes correspondent aux attentes
DESCRIBE TABLE events FORMAT Vertical

-- Insérer des données et exécuter une requête pour valider que le schéma les prend bien en charge
INSERT INTO events FORMAT JSONEachRow
{"timestamp":"2025-03-19 10:00:00","service":"api","level":"INFO","message":"request handled","host":"node-1","duration_ms":42}

SELECT service, level, duration_ms FROM events WHERE service = 'api'

Points d’attention

  • Si vous insérez des données JSON avec JSONEachRow et que le JSON contient des champs absents du schéma, ClickHouse les ignore silencieusement par défaut. Définissez input_format_skip_unknown_fields sur 0 si vous préférez obtenir des erreurs.

Hybride (colonnes typées + JSON)

Quand l’utiliser : Un ensemble de champs de base est stable (timestamp, ID, codes d’état), mais une partie du payload est dynamique. Pensez à des attributs définis par l’utilisateur, des tags, des métadonnées ou des champs d’extension qui varient d’un enregistrement à l’autre.

Compromis : Performances maximales sur les colonnes typées, flexibilité sur la colonne JSON. La colonne JSON implique malgré tout une surcharge à l’insert et un coût de stockage pour sa partie dynamique.

Configuration, vérification et points d’attention

Configuration

CREATE TABLE events
(
    `timestamp`  DateTime,
    `service`    LowCardinality(String),
    `level`      Enum8('DEBUG' = 1, 'INFO' = 2, 'WARN' = 3, 'ERROR' = 4),
    `message`    String,
    `host`       LowCardinality(String),
    `duration_ms` UInt32,
    `attributes` JSON(
        max_dynamic_paths = 256,
        `http.status_code` UInt16,
        `http.method` LowCardinality(String),
        SKIP REGEXP 'debug\..*'
    )
)
ENGINE = MergeTree
ORDER BY (service, timestamp)

Vérification

-- Insérer des données d’exemple et inspecter les chemins inférés
INSERT INTO events FORMAT JSONEachRow
{"timestamp":"2025-03-19 10:00:00","service":"api","level":"INFO","message":"request handled","host":"node-1","duration_ms":42,"attributes":{"http.status_code":200,"http.method":"GET","user.region":"eu-west","custom_tag":"abc"}}

SELECT JSONAllPathsWithTypes(attributes)
FROM events
FORMAT PrettyJSONEachRow

Points d’attention

  • Utilisez des indications de type sur les chemins JSON que vous connaissez à l’avance. Elles contournent la colonne discriminante et stockent le chemin comme une colonne typée classique, avec les mêmes performances et sans surcharge.
  • Utilisez SKIP ou SKIP REGEXP pour les chemins que vous n’interrogez jamais (métadonnées de débogage, ID internes de tracing) afin d’économiser de l’espace de stockage et de réduire le nombre de sous-colonnes.
  • Définissez max_dynamic_paths en fonction du nombre de chemins distincts que vous interrogez réellement. La valeur par défaut (1024) convient dans la plupart des cas. Réduisez-la si votre section dynamique est limitée.
  • Ne définissez pas max_dynamic_paths au-delà de 10 000. Des valeurs élevées augmentent la consommation de ressources et réduisent l’efficacité.

Colonne JSON native

Quand l’utiliser : La structure est véritablement imprévisible, avec des clés qui apparaissent et disparaissent d’un enregistrement à l’autre. Schémas générés par les utilisateurs, systèmes de plugins ou ingestion dans un lac de données lorsque vous ne contrôlez pas le schéma en amont.

Compromis : Les insertions sont plus lentes qu’avec des colonnes typées. Les lectures de l’objet complet sont plus lentes qu’avec String. Surcoût de stockage lié à la gestion des sous-colonnes. Fonctionne bien pour les requêtes au niveau des champs sur des chemins spécifiques.

Configuration, vérification et points d’attention

Configuration

CREATE TABLE dynamic_events
(
    `id`   UInt64,
    `ts`   DateTime DEFAULT now(),
    `data` JSON(
        max_dynamic_paths = 512,
        `event_type` LowCardinality(String),
        `version` UInt8
    )
)
ENGINE = MergeTree
ORDER BY (data.event_type, ts)

Utilisez le format JSONAsObject lors de l’insertion de documents JSON complets dans une colonne JSON. Il traite chaque ligne d’entrée comme un objet JSON complet associé à la colonne.

Vérification

INSERT INTO dynamic_events (id, data) FORMAT JSONEachRow
{"id": 1, "data": {"event_type": "click", "version": 2, "page": "/home", "button_id": "cta-1"}}
{"id": 2, "data": {"event_type": "purchase", "version": 1, "item_id": "SKU-99", "amount": 49.99, "currency": "USD"}}

-- Vérifiez quels chemins ClickHouse a détectés et leurs types
SELECT JSONAllPathsWithTypes(data) FROM dynamic_events FORMAT PrettyJSONEachRow

-- Interrogez un chemin spécifique
SELECT data.page FROM dynamic_events WHERE data.event_type = 'click'

Points d’attention

  • Sans indications de type, ClickHouse déduit les types chemin par chemin à partir des premières valeurs observées. Si score arrive sous la forme "10" (chaîne) dans un enregistrement et 10 (entier) dans un autre, le chemin reçoit une colonne discriminante et les requêtes deviennent plus lentes. Ajoutez des indications pour les chemins dont les types sont connus.
  • Lorsque le nombre de chemins dépasse max_dynamic_paths, les valeurs excédentaires sont déplacées vers une shared data structure, ce qui réduit les performances des requêtes. Surveillez cela avec JSONDynamicPaths() et maintenez la limite sous 10 000.
  • Chaque chemin dynamique prend en charge jusqu’à max_dynamic_types (32 par défaut) types de données distincts. Si un même chemin dépasse cette limite, les types supplémentaires basculent vers un stockage Variant partagé. Cela a rarement de l’importance, sauf si vos données présentent des types très incohérents pour un même champ.

Stockage opaque en String

Quand l'utiliser : les documents JSON sont stockés et récupérés en bloc, puis transmis à une application, archivés ou relayés en aval. Aucun filtrage ni aucune agrégation au niveau des champs dans ClickHouse.

Compromis : insertions les plus rapides et schéma le plus simple. Pas de requêtes au niveau des champs sans analyse à l'exécution (famille JSONExtract), ce qui est lent à grande échelle.

Configuration, vérification et pièges à éviter

Configuration

CREATE TABLE raw_events
(
    `id`        UInt64,
    `received`  DateTime DEFAULT now(),
    `payload`   String
)
ENGINE = MergeTree
ORDER BY (received)

Vérification

INSERT INTO raw_events (id, payload) VALUES
(1, '{"type":"click","page":"/home"}'),
(2, '{"type":"purchase","item":"SKU-99","amount":49.99}')

-- Vérifiez que les données sont bien restituées à l'identique
SELECT payload FROM raw_events WHERE id = 1

-- Vérifiez que vous pouvez toujours extraire des champs à la demande si nécessaire
SELECT JSONExtractString(payload, 'type') AS event_type FROM raw_events

Points de vigilance

  • Si les besoins évoluent et que vous devez ensuite effectuer des requêtes au niveau des champs, il vous faudra créer une nouvelle table avec des colonnes typées ou JSON, puis backfill les données. S'il y a la moindre chance que vous interrogiez des champs individuels, privilégiez plutôt l'approche hybride.
  • Les fonctions JSONExtract analysent la chaîne à chaque requête. C'est acceptable pour une exploration ad hoc, mais pas pour des dashboards de production ni pour des workloads à QPS élevé.
  • Envisagez des codecs de compression (ZSTD) sur la colonne String si les payloads JSON sont volumineux : la compression est efficace.

Comparaison

Critère Colonnes typées Hybride JSON natif String
Débit d'insertion Le plus rapide Rapide Modéré Le plus rapide
Requêtes au niveau des champs Le plus rapide Rapides (typées) ; bonnes (JSON avec hint) Bonnes (avec hint) ; plus lentes (Dynamic) Lentes (analyse à l'exécution)
Lectures de l'objet complet Rapides Modérées Lentes Les plus rapides
Efficacité du stockage La meilleure Bonne Modérée Bonne (bonne compression)
Flexibilité du schéma Aucune (ALTER TABLE) Partielle (cœur rigide, extension flexible) Complète Complète
Complexité Faible Moyenne Moyenne à élevée Faible

Quand Map convient mieux

Si vos champs dynamiques sont des paires clé-valeur homogènes — c’est-à-dire que toutes les valeurs ont le même type — Map(String, T) est plus simple et plus efficace qu’une colonne JSON. Exemples courants : des tags de type chaîne (Map(String, String)), des métriques numériques (Map(String, Float64)) ou des feature flags (Map(String, Bool)).

CREATE TABLE tagged_events
(
    `timestamp` DateTime,
    `service`   LowCardinality(String),
    `tags`      Map(String, String)  -- e.g., {"env": "prod", "region": "us-east-1", "team": "platform"}
)
ENGINE = MergeTree
ORDER BY (service, timestamp)

Map prend en charge le filtrage au niveau des clés (tags['env'] = 'prod'), coûte moins cher à stocker que JSON et évite la surcharge liée aux sous-colonnes du type JSON. Notez que, par défaut, les recherches de clés parcourent la map linéairement — cela convient pour de petits ensembles de tags, mais pour les maps de plus de 100 clés, envisagez la sérialisation with_buckets. Utilisez JSON lorsque les valeurs ont des types hétérogènes ou que la structure est imbriquée — utilisez Map lorsqu’il s’agit de paires clé-valeur simples avec un type de valeur uniforme.

Navigation