Passer au contenu principal
Bien que l’inférence de schéma puisse être utilisée pour définir un schéma initial pour des données JSON et interroger directement des fichiers de données JSON, par exemple dans S3, vous devriez viser à définir pour vos données un schéma versionné optimisé. Nous présentons ci-dessous l’approche recommandée pour modéliser les structures JSON.

JSON statique ou dynamique

Lors de la définition d’un schéma pour du JSON, la tâche principale consiste à déterminer le type approprié pour la valeur de chaque clé. Nous recommandons d’appliquer récursivement les règles suivantes à chaque clé de la hiérarchie JSON afin de déterminer le type approprié pour chacune d’elles.
  1. Types primitifs - Si la valeur de la clé est d’un type primitif, qu’elle fasse partie d’un sous-objet ou qu’elle se trouve à la racine, veillez à choisir son type conformément aux bonnes pratiques générales de conception de schéma et aux règles d’optimisation des types. Les tableaux de primitives, comme phone_numbers ci-dessous, peuvent être modélisés sous la forme Array(<type>), par exemple Array(String).
  2. Statique ou dynamique - Si la valeur de la clé est un objet complexe, c.-à-d. soit un objet, soit un tableau d’objets, déterminez si elle est susceptible d’évoluer. Les objets qui reçoivent rarement de nouvelles clés, pour lesquels l’ajout d’une nouvelle clé peut être anticipé et géré par une modification du schéma via ALTER TABLE ADD COLUMN, peuvent être considérés comme statiques. Cela inclut les objets pour lesquels seul un sous-ensemble des clés peut être fourni dans certains documents JSON. Les objets auxquels de nouvelles clés sont ajoutées fréquemment et/ou de manière imprévisible doivent être considérés comme dynamiques. L’exception concerne ici les structures comportant des centaines ou des milliers de sous-clés, qui peuvent être considérées comme dynamiques par souci de simplicité.
Pour déterminer si une valeur est statique ou dynamique, consultez les sections correspondantes Gestion des objets statiques et Gestion des objets dynamiques ci-dessous.

Important : Les règles ci-dessus doivent être appliquées récursivement. Si la valeur d’une clé est jugée dynamique, aucune évaluation supplémentaire n’est nécessaire et vous pouvez suivre les recommandations de Gestion des objets dynamiques. Si l’objet est statique, continuez à évaluer les sous-clés jusqu’à rencontrer soit des valeurs primitives, soit des clés dynamiques. Pour illustrer ces règles, nous utilisons l’exemple JSON suivant représentant une personne :
En appliquant ces règles :
  • Les clés racines name, username, email, website peuvent être représentées par le type String. La colonne phone_numbers est une primitive Array de type Array(String), tandis que dob et id sont de type Date et UInt32, respectivement.
  • Aucune nouvelle clé ne sera ajoutée à l’objet address (seuls de nouveaux objets d’adresse pourront l’être) ; il peut donc être considéré comme statique. Si l’on descend récursivement, toutes les sous-colonnes peuvent être considérées comme des primitives (et de type String), à l’exception de geo. Il s’agit également d’une structure statique avec deux colonnes Float32, lat et lon.
  • La colonne tags est dynamique. Nous supposons que de nouveaux tags arbitraires, de tout type et de toute structure, peuvent être ajoutés à cet objet.
  • L’objet company est statique et contiendra toujours au plus les 3 clés indiquées. Les sous-clés name et catchPhrase sont de type String. La clé labels est dynamique. Nous supposons que de nouveaux tags arbitraires peuvent être ajoutés à cet objet. Les valeurs seront toujours des paires clé-valeur de type chaîne.
Les structures comportant des centaines ou des milliers de clés statiques peuvent être considérées comme dynamiques, car il est rarement envisageable d’en déclarer statiquement les colonnes. Cependant, lorsque c’est possible, ignorez les chemins inutiles afin de réduire à la fois le stockage et le surcoût lié à l’inférence.

Gestion des structures statiques

Nous recommandons de gérer les structures statiques à l’aide de tuples nommés, c’est-à-dire Tuple. Les tableaux d’objets peuvent être stockés sous forme de tableaux de tuples, c’est-à-dire Array(Tuple). Au sein même des tuples, les colonnes et leurs types respectifs doivent être définis selon les mêmes règles. Cela peut conduire à des Tuples imbriqués pour représenter des objets imbriqués, comme illustré ci-dessous. Pour l’illustrer, nous reprenons l’exemple JSON de personne présenté précédemment, en omettant les objets dynamiques :
Le schéma de cette table est présenté ci-dessous :
Notez que la colonne company est définie comme un Tuple(catchPhrase String, name String). La clé address utilise un Array(Tuple), avec un Tuple imbriqué pour représenter la colonne geo. Le JSON peut être inséré dans cette table avec sa structure actuelle :
Dans notre exemple ci-dessus, nous avons peu de données, mais comme indiqué ci-dessous, nous pouvons interroger les colonnes du tuple à l’aide de leurs noms séparés par des points.
Notez que la colonne address.street est renvoyée en tant qu’Array. Pour interroger un objet précis dans un tableau en fonction de sa position, il faut indiquer l’indice du tableau après le nom de la colonne. Par exemple, pour accéder à la rue de la première adresse :
Les sous-colonnes peuvent aussi être utilisées dans les clés de tri à partir de la version 24.12 :

Gestion des valeurs par défaut

Même si les objets JSON sont structurés, ils sont souvent peu renseignés et seul un sous-ensemble des clés connues est fourni. Heureusement, le type Tuple n’exige pas que toutes les colonnes soient présentes dans la charge utile JSON. Si elles ne sont pas fournies, des valeurs par défaut seront utilisées. Prenons notre table people précédente et le JSON partiel suivant, dans lequel les clés suite, geo, phone_numbers et catchPhrase sont absentes.
On peut voir ci-dessous que cette ligne peut être insérée avec succès :
En interrogeant cette ligne unique, nous pouvons constater que des valeurs par défaut sont utilisées pour les colonnes (y compris les sous-objets) qui ont été omises :
Différencier les valeurs vides et nullesSi vous devez distinguer une valeur vide d’une valeur non fournie, vous pouvez utiliser le type Nullable. Cette approche est à éviter sauf en cas de nécessité absolue, car elle a un impact négatif sur le stockage et les performances des requêtes pour ces colonnes.

Gestion des nouvelles colonnes

Bien qu’une approche structurée soit la plus simple lorsque les clés JSON sont statiques, elle peut également être utilisée si les modifications du schéma peuvent être planifiées, c’est-à-dire si les nouvelles clés sont connues à l’avance et que le schéma peut être modifié en conséquence. Notez que ClickHouse ignore par défaut les clés JSON fournies dans la charge utile et absentes du schéma. Prenons l’exemple de la charge utile JSON modifiée suivante, avec l’ajout d’une clé nickname :
Ce JSON peut être inséré avec succès en ignorant la clé nickname :
Des colonnes peuvent être ajoutées à un schéma à l’aide de la commande ALTER TABLE ADD COLUMN. Une valeur par défaut peut être spécifiée via la clause DEFAULT ; elle sera utilisée si elle n’est pas indiquée lors des insertions suivantes. Les lignes pour lesquelles cette valeur n’est pas présente (car elles ont été insérées avant sa création) renverront également cette valeur par défaut. Si aucune valeur DEFAULT n’est spécifiée, la valeur par défaut du type sera utilisée. Par exemple :

Gestion des structures semi-structurées/dynamiques

Si les données JSON sont semi-structurées, avec des clés qui peuvent être ajoutées dynamiquement et/ou avoir plusieurs types, le type JSON est recommandé. Plus précisément, utilisez le type JSON lorsque vos données :
  • ont des clés imprévisibles susceptibles d’évoluer au fil du temps ;
  • contiennent des valeurs de types variables (par exemple, un chemin peut parfois contenir une chaîne, parfois un nombre) ;
  • nécessitent une flexibilité de schéma lorsqu’un typage strict n’est pas envisageable ;
  • comportent des centaines, voire des milliers de chemins statiques qu’il n’est tout simplement pas réaliste de déclarer explicitement. Ce cas reste toutefois rare.
Reprenons notre exemple précédent de JSON person, dans lequel l’objet company.labels a été identifié comme dynamique. Supposons que company.labels contienne des clés arbitraires. De plus, le type de n’importe quelle clé de cette structure peut ne pas être cohérent d’une ligne à l’autre. Par exemple :
Compte tenu de la nature dynamique de la colonne company.labels d’un objet à l’autre, tant du point de vue des clés que des types, nous avons plusieurs options pour modéliser ces données :
  • Colonne JSON unique - représente l’ensemble du schéma dans une seule colonne JSON, ce qui permet à toutes les structures qu’elle contient d’être dynamiques.
  • Colonne JSON ciblée - utilise uniquement le type JSON pour la colonne company.labels, tout en conservant le schéma structuré présenté ci-dessus pour toutes les autres colonnes.
Bien que la première approche ne s’aligne pas sur la méthodologie précédente, l’approche de la colonne JSON unique est utile pour le prototypage et les tâches d’ingénierie des données. Pour les déploiements de ClickHouse en production à grande échelle, nous recommandons de définir une structure précise et d’utiliser le type JSON pour les sous-structures dynamiques ciblées lorsque c’est possible. Un schéma strict présente plusieurs avantages :
  • Validation des données – l’application d’un schéma strict évite le risque d’explosion du nombre de colonnes, sauf pour certaines structures spécifiques.
  • Évite le risque d’explosion du nombre de colonnes - Bien que le type JSON puisse monter jusqu’à des milliers de colonnes, les sous-colonnes étant stockées comme des colonnes dédiées, cela peut entraîner une explosion du nombre de fichiers de colonnes, avec la création d’un nombre excessif de fichiers qui nuit aux performances. Pour atténuer ce problème, le type Dynamic sous-jacent utilisé par JSON propose un paramètre max_dynamic_paths, qui limite le nombre de chemins uniques stockés dans des fichiers de colonnes distincts. Une fois le seuil atteint, les chemins supplémentaires sont stockés dans un fichier de colonnes partagé à l’aide d’un format compact encodé, ce qui préserve les performances et l’efficacité du stockage tout en prenant en charge une ingestion de données flexible. L’accès à ce fichier de colonnes partagé est toutefois moins performant. Notez toutefois que la colonne JSON peut être utilisée avec des indications de type. Les colonnes « indiquées » offriront les mêmes performances que les colonnes dédiées.
  • Introspection plus simple des chemins et des types - Bien que le type JSON prenne en charge des fonctions d’introspection pour déterminer les types et les chemins qui ont été inférés, les structures statiques peuvent être plus simples à explorer, par exemple avec DESCRIBE.

Colonne JSON unique

Cette approche est utile pour le prototypage et les tâches d’ingénierie des données. En production, n’utilisez JSON que pour les sous-structures dynamiques, lorsque c’est nécessaire.
Considérations relatives aux performancesUne colonne JSON unique peut être optimisée en ignorant (c’est-à-dire sans stocker) les chemins JSON qui ne sont pas nécessaires et en utilisant des indications de type. Les indications de type permettent à l’utilisateur de définir explicitement le type d’une sous-colonne, évitant ainsi l’inférence et le traitement de l’indirection au moment de la requête. On peut ainsi obtenir les mêmes performances qu’avec un schéma explicite. Voir « Using type hints and skipping paths » pour plus de détails.
Le schéma d’une colonne JSON unique est simple :
Nous fournissons une indication de type pour la colonne username dans la définition JSON, car nous l’utilisons dans la clé de tri / clé primaire. Cela permet à ClickHouse de savoir que cette colonne ne sera pas NULL et de déterminer quelle sous-colonne username utiliser (il peut y en avoir plusieurs pour chaque type, ce qui serait sinon ambigu).
L’insertion de lignes dans la table ci-dessus peut se faire à l’aide du format JSONAsObject :
Nous pouvons déterminer les sous-colonnes inférées et leurs types à l’aide des fonctions d’introspection. Par exemple :
Pour une liste complète des fonctions d’introspection, voir les “Fonctions d’introspection” Les sous-chemins sont accessibles avec la notation . p. ex.
Notez que les colonnes absentes de certaines lignes sont renvoyées sous la forme de NULL. De plus, une sous-colonne distincte est créée pour les chemins de même type. Par exemple, il existe une sous-colonne pour company.labels.type en String et une autre en Array(Nullable(String)). Bien que les deux soient renvoyées lorsque c’est possible, nous pouvons cibler des sous-colonnes spécifiques à l’aide de la syntaxe .: :
Pour renvoyer des sous-objets imbriqués, le caractère ^ est nécessaire. Il s’agit d’un choix de conception destiné à éviter de lire un grand nombre de colonnes, sauf demande explicite. Les objets auxquels on accède sans ^ renverront NULL, comme indiqué ci-dessous :

Colonne JSON ciblée

Bien qu’utile pour le prototypage et les tâches d’ingénierie des données, nous recommandons d’utiliser un schéma explicite en production dans la mesure du possible. Notre exemple précédent peut être modélisé avec une seule colonne JSON pour la colonne company.labels.
Nous pouvons insérer des données dans cette table en utilisant le format JSONEachRow :
Les fonctions d’introspection permettent de déterminer les chemins et types inférés pour la colonne company.labels.

Utilisation des indications de type et des chemins à ignorer

Les indications de type permettent de spécifier le type d’un chemin et de sa sous-colonne, ce qui évite une inférence de type inutile. Prenons l’exemple suivant, dans lequel nous définissons les types des clés JSON dissolved, employees et founded dans la colonne JSON company.labels
Remarquez que ces colonnes ont désormais les types explicites que nous avons définis :
De plus, nous pouvons ignorer des chemins dans le JSON que nous ne voulons pas stocker à l’aide des paramètres SKIP et SKIP REGEXP, afin de réduire le stockage et d’éviter une inférence inutile sur des chemins dont nous n’avons pas besoin. Par exemple, supposons que nous utilisions une seule colonne JSON pour les données ci-dessus. Nous pouvons ignorer les chemins address et company :
Notez que nos colonnes ont été exclues des données :

Optimiser les performances avec des indications de type

Les indications de type ne servent pas seulement à éviter une inférence de type inutile : elles suppriment entièrement l’indirection liée au stockage et au traitement, tout en permettant de spécifier des types primitifs optimaux. Les chemins JSON dotés d’indications de type sont toujours stockés comme des colonnes traditionnelles, ce qui évite d’avoir recours à des colonnes discriminantes ou à une résolution dynamique au moment de l’exécution des requêtes. Cela signifie qu’avec des indications de type bien définies, les clés JSON imbriquées offrent les mêmes performances et la même efficacité que si elles avaient été modélisées dès le départ comme des colonnes de premier niveau. Ainsi, pour les jeux de données globalement cohérents qui bénéficient malgré tout de la flexibilité de JSON, les indications de type constituent un moyen pratique de préserver les performances sans avoir à restructurer votre schéma ni votre pipeline d’ingestion.

Configuration des chemins dynamiques

ClickHouse stocke chaque chemin JSON sous forme de sous-colonne dans une véritable structure columnar, ce qui permet de bénéficier des mêmes gains de performance qu’avec des colonnes traditionnelles — notamment la compression, le traitement accéléré par SIMD et des E/S disque minimales. Chaque combinaison unique de chemin et de type dans vos données JSON peut devenir son propre fichier de colonne sur disque. Par exemple, lorsque deux chemins JSON sont insérés avec des types différents, ClickHouse stocke les valeurs de chaque type concret dans des sous-colonnes distinctes. Ces sous-colonnes peuvent être consultées indépendamment, ce qui limite les E/S inutiles. Notez toutefois que lors de l’interrogation d’une colonne contenant plusieurs types, les valeurs sont toujours renvoyées sous la forme d’une seule réponse columnar. En outre, grâce aux offsets, ClickHouse garantit que ces sous-colonnes restent denses, sans stocker de valeurs par défaut pour les chemins JSON absents. Cette approche maximise la compression et réduit encore les E/S. Cependant, dans les scénarios avec des structures JSON à forte cardinalité ou très variables — comme les pipelines de télémétrie, les logs ou les feature stores de machine learning — ce comportement peut entraîner une explosion du nombre de fichiers de colonnes. Chaque nouveau chemin JSON unique crée un nouveau fichier de colonne, et chaque variante de type sous ce chemin crée un fichier de colonne supplémentaire. Bien que cela soit optimal pour les performances de lecture, cela pose des difficultés opérationnelles : épuisement des descripteurs de fichiers, augmentation de l’utilisation mémoire et merges plus lents en raison d’un grand nombre de petits fichiers. Pour atténuer ce problème, ClickHouse introduit le concept de sous-colonne de débordement : une fois que le nombre de chemins JSON distincts dépasse un seuil, les chemins supplémentaires sont stockés dans un seul fichier partagé à l’aide d’un format compact encodé. Ce fichier reste interrogeable, mais ne bénéficie pas des mêmes caractéristiques de performance que les sous-colonnes dédiées. Ce seuil est contrôlé par le paramètre max_dynamic_paths dans la déclaration du type JSON.
Évitez de définir ce paramètre sur une valeur trop élevée - des valeurs élevées augmentent la consommation de ressources et réduisent l’efficacité. En règle générale, gardez-le en dessous de 10 000. Pour les charges de travail aux structures très dynamiques, utilisez des indications de type et des paramètres SKIP pour limiter ce qui est stocké. Pour les utilisateurs qui souhaitent en savoir plus sur l’implémentation de ce nouveau type de colonne, nous recommandons la lecture de notre article de blog détaillé “A New Powerful JSON Data Type for ClickHouse”.
Dernière modification le 29 juin 2026