Vous cherchez un guide ?
Consultez notre guide des bonnes pratiques pour JSON, avec des exemples, des fonctionnalités avancées et des points à prendre en compte lors de l’utilisation du type JSON.
JSON stocke des documents JavaScript Object Notation (JSON) dans une seule colonne.
Dans ClickHouse Open-Source, le type de données JSON est considéré comme prêt pour la production à partir de la version 25.3. Il n’est pas recommandé d’utiliser ce type en production dans les versions antérieures.
JSON, vous pouvez utiliser la syntaxe suivante :
Quand utiliser le type JSON
JSON est conçu pour interroger, filtrer et agréger des champs spécifiques au sein d’objets JSON dont la structure est dynamique ou imprévisible. Il y parvient en décomposant les objets JSON en sous-colonnes distinctes, ce qui réduit considérablement le volume de données lu et accélère les requêtes sur les champs sélectionnés par rapport à des alternatives comme Map ou l’analyse de chaînes de caractères.
Cependant, cela implique des compromis importants :
INSERTplus lents - Décomposer le JSON en sous-colonnes, effectuer l’inférence de type et gérer des structures de stockage flexibles ralentit les insertions par rapport au stockage du JSON dans une simple colonneString.- Plus lent pour la lecture d’objets entiers - Si vous devez récupérer des documents JSON complets (plutôt que des champs spécifiques), le type
JSONest plus lent qu’une lecture depuis une colonneString. Le surcoût lié à la reconstruction des objets à partir de sous-colonnes distinctes n’apporte aucun bénéfice lorsque vous n’effectuez pas de requêtes au niveau des champs. - Surcoût de stockage - Le maintien de sous-colonnes distinctes ajoute un surcoût structurel par rapport au stockage du JSON sous la forme d’une unique valeur de chaîne.
Utilisez le type JSON lorsque :
- Vos données ont une structure dynamique ou imprévisible, avec des clés qui varient d’un document à l’autre
- Les types de champs ou les schémas évoluent au fil du temps, ou varient d’un enregistrement à l’autre
- Vous devez interroger, filtrer ou agréger des chemins spécifiques au sein d’objets JSON dont vous ne pouvez pas prévoir la structure à l’avance
- Votre cas d’utilisation porte sur des données semi-structurées, comme des logs, des événements ou du contenu généré par les utilisateurs, avec des schémas incohérents
Utilisez une colonne String (ou des types structurés) lorsque :
- La structure de vos données est connue et stable ; dans ce cas, utilisez plutôt des colonnes classiques ou les types
Tuple,Array,DynamicouVariant - Les documents
JSONsont traités comme des blobs opaques, uniquement stockés et récupérés dans leur intégralité, sans analyse au niveau des champs - Vous n’avez pas besoin d’interroger ni de filtrer des champs
JSONindividuels dans la base de données - Le
JSONsert simplement de format de transport/stockage et n’est pas analysé dans ClickHouse
Créer du JSON
JSON.
Utiliser JSON dans la définition d’une colonne d’une table
requête (Example 1)
réponse (Example 1)
requête (Example 2)
réponse (Example 2)
Utiliser CAST avec ::JSON
::JSON.
CAST de String en JSON
requête
réponse
CAST de Tuple en JSON
requête
réponse
CAST de Map en JSON
requête
réponse
Les chemins JSON sont stockés à plat. Cela signifie que lorsqu’un objet JSON est reconstitué à partir d’un chemin comme renverra :et non :
a.b.c,
il n’est pas possible de savoir s’il doit être construit sous la forme { "a.b.c" : ... } ou { "a": { "b": { "c": ... } } }.
Notre implémentation supposera toujours le second cas.Par exemple :Requête
Réponse
Lecture des chemins JSON en tant que sous-colonnes
JSON permet de lire chaque chemin comme une sous-colonne distincte.
Si le type du chemin demandé n’est pas spécifié dans la déclaration du type JSON,
la sous-colonne correspondante aura toujours le type Dynamic.
Par exemple :
requête
réponse
requête (Reading JSON paths as sub-columns)
réponse (Reading JSON paths as sub-columns)
getSubcolumn pour lire des sous-colonnes à partir du type JSON :
requête
réponse
NULL :
requête
réponse
requête
réponse
a.b, le type est UInt32, comme nous l’avons spécifié dans la déclaration du type JSON,
et pour toutes les autres sous-colonnes, le type est Dynamic.
Il est également possible de lire les sous-colonnes d’un type Dynamic à l’aide de la syntaxe spéciale json.some.path.:TypeName :
requête
réponse
Dynamic peuvent être converties dans n’importe quel type de données. Dans ce cas, une exception sera levée si le type interne de Dynamic ne peut pas être converti dans le type demandé :
requête
réponse
requête
réponse
Pour lire efficacement les sous-colonnes à partir de parts Compact MergeTree, assurez-vous que le paramètre MergeTree write_marks_for_substreams_in_compact_parts est activé.
Lecture des sous-objets JSON en tant que sous-colonnes
JSON prend en charge la lecture d’objets imbriqués en tant que sous-colonnes de type JSON à l’aide de la syntaxe spéciale json.^some.path :
requête
réponse
requête
réponse
Lorsque les chemins sont stockés dans des données partagées de base (
map), la lecture des sous-colonnes de sous-objets peut être inefficace, car elle nécessite de parcourir l’ensemble de la structure de données partagées. Avec la sérialisation des données partagées map_with_buckets ou advanced, la lecture des sous-colonnes à partir des données partagées est fortement optimisée.Lecture des sous-colonnes combinées JSON
JSON permet de lire un chemin sous forme de sous-colonne combinée à l’aide de la syntaxe spéciale json.@some.path.
Pour un chemin donné, une sous-colonne combinée renvoie :
- La valeur littérale stockée à ce chemin sous forme de
Dynamic, si le chemin contient une valeur littérale. - Un sous-objet JSON à ce chemin sous forme de
Dynamic, si le chemin ne contient pas de valeur littérale mais comporte des sous-chemins imbriqués. NULL, si ce chemin ne contient ni valeur littérale ni sous-chemins.
json.a) et la sous-colonne de sous-objet (json.^a).
L’exemple suivant compare les trois types de sous-colonnes pour le chemin a :
requête
réponse
requête
réponse
- Ligne 1 :
acontient le littéral42.json.ale renvoie commeDynamic(Int64),json.^arenvoie un sous-objet vide{}(aucune clé imbriquée sousa), etjson.@arenvoie le littéral42. - Ligne 2 :
acontient un objet imbriqué.json.arenvoieNULL(aucun littéral à ce chemin),json.^arenvoie le sous-objet au formatJSON, etjson.@arenvoie également le sous-objet commeDynamic(JSON). - Ligne 3 :
aest totalement absent.json.aetjson.@arenvoient tous deuxNULL, tandis quejson.^arenvoie un objet vide{}.
Lorsque des chemins sont stockés dans les données partagées de base (
map), la lecture des sous-colonnes combinées peut être inefficace, car elle nécessite de parcourir l’intégralité de la structure de données partagées. Avec la sérialisation des données partagées map_with_buckets ou advanced, la lecture des sous-colonnes depuis les données partagées est très optimisée.Inférence de type pour les chemins
JSON, ClickHouse essaie de détecter le type de données le plus approprié pour chaque chemin JSON.
Cela fonctionne de manière similaire à l’inférence automatique du schéma à partir des données d’entrée,
et est contrôlé par les mêmes paramètres :
- input_format_try_infer_dates
- input_format_try_infer_datetimes
- schema_inference_make_columns_nullable
- input_format_json_try_infer_numbers_from_strings
- input_format_json_infer_incomplete_types_as_strings
- input_format_json_read_numbers_as_strings
- input_format_json_read_bools_as_strings
- input_format_json_read_bools_as_numbers
- input_format_json_read_arrays_as_strings
- input_format_json_infer_array_of_dynamic_from_array_of_different_types
requête
réponse
requête
réponse
requête
réponse
requête
réponse
Gestion des tableaux d’objets JSON
Array(JSON) et insérés dans une colonne Dynamic pour ce chemin.
Pour lire un tableau d’objets, vous pouvez l’extraire de la colonne Dynamic sous forme de sous-colonne :
requête
réponse
requête
réponse
max_dynamic_types/max_dynamic_paths du type JSON imbriqué ont été réduits par rapport aux valeurs par défaut.
Cela est nécessaire pour éviter que le nombre de sous-colonnes n’augmente de façon incontrôlée dans des tableaux imbriqués d’objets JSON.
Essayons de lire des sous-colonnes depuis une colonne JSON imbriquée :
requête
Response
Array(JSON) grâce à une syntaxe spéciale :
Query
Response
[] après le chemin indique le niveau du tableau. Par exemple, json.path[][] sera transformé en json.path.:Array(Array(JSON))
Examinons les chemins et les types à l’intérieur de notre Array(JSON) :
Query
Response
Array(JSON) :
Query
Response
JSON imbriquée :
Query
Response
Gestion des clés JSON avec NULL
null et l’absence de valeur sont considérés comme équivalents :
Query
Response
Gestion des clés JSON contenant des points
a.b et de la valeur 42. Lors de la mise en forme du JSON, nous construisons toujours des objets imbriqués à partir des segments du chemin séparés par des points :
Query
Response
{"a.b" : 42} est désormais formaté ainsi : {"a" : {"b" : 42}}.
Cette limitation empêche également l’analyse d’objets JSON valides comme celui-ci :
Query
Response
25.8). Dans ce cas, lors de l’analyse, tous les points dans les clés JSON seront
échappés en %2E, puis restaurés lors du formatage.
Query
Response
Query
Response
Query
Response
json.`a.b` est équivalente à la sous-colonne json.a.b et n’interprétera pas le chemin contenant un point d’échappement :
Query
Response
SKIP/SKIP REGEX), vous devez utiliser des points échappés dans l’indice :
Query
Response
Query
Response
Lecture du type JSON à partir de données
JSONEachRow,
TSV,
CSV,
CustomSeparated,
Values, etc.) permettent de lire le type JSON.
Exemples :
Query
Response
CSV/TSV/etc., JSON est analysé à partir d’une chaîne contenant l’objet JSON :
Query
Response
Atteindre la limite des chemins dynamiques dans JSON
JSON ne peut stocker en interne qu’un nombre limité de chemins sous forme de sous-colonnes distinctes.
Par défaut, cette limite est fixée à 1024, mais vous pouvez la modifier dans la déclaration du type à l’aide du paramètre max_dynamic_paths.
Lorsque cette limite est atteinte, tous les nouveaux chemins insérés dans une colonne JSON sont stockés dans une structure de données partagée unique.
Il reste possible de lire ces chemins comme des sous-colonnes,
mais cela peut être moins efficace (voir la section sur la structure de données partagée).
Cette limite est nécessaire pour éviter d’avoir un nombre énorme de sous-colonnes différentes, ce qui pourrait rendre la table inutilisable.
Voyons ce qui se passe lorsque la limite est atteinte dans différents scénarios.
Atteinte de la limite lors de l’analyse des données
JSON à partir des données, lorsque la limite est atteinte pour le bloc de données en cours,
tous les nouveaux chemins sont stockés dans une structure de données partagée. Nous pouvons utiliser les deux fonctions d’introspection suivantes : JSONDynamicPaths, JSONSharedDataPaths
Query
Response
e et f.g, la limite a été atteinte,
et ils ont été insérés dans une structure de données partagées.
Lors des fusions de parties de données dans les moteurs de table MergeTree
MergeTree, la colonne JSON de la partie de données résultante peut atteindre la limite de chemins dynamiques
et ne plus pouvoir stocker tous les chemins des parties sources sous forme de sous-colonnes.
Dans ce cas, ClickHouse choisit quels chemins seront conservés comme sous-colonnes après la fusion et quels chemins seront stockés dans la structure de données partagée.
Dans la plupart des cas, ClickHouse essaie de conserver les chemins qui contiennent
le plus grand nombre de valeurs non NULL et de déplacer les chemins les plus rares vers la structure de données partagée. Cela dépend toutefois de l’implémentation.
Voyons un exemple d’une telle fusion.
Commençons par créer une table avec une colonne JSON, définir la limite de chemins dynamiques à 3, puis insérer des valeurs avec 5 chemins différents :
Query
JSON ne contenant qu’un seul chemin :
Query
Response
Query
Response
a, b et c, et a déplacé les chemins d et e vers une structure de données partagée.
Comme indiqué dans la section précédente, lorsque la limite max_dynamic_paths est atteinte, tous les nouveaux chemins sont stockés dans une structure de données partagée unique.
Dans cette section, nous allons examiner en détail la structure de données partagée et la manière dont les sous-colonnes des chemins y sont lues.
Voir la section “fonctions d’introspection” pour plus de détails sur les fonctions utilisées pour inspecter le contenu d’une colonne JSON.
En mémoire, la structure de données partagée n’est rien d’autre qu’une sous-colonne de type Map(String, String) qui stocke la correspondance entre un chemin JSON aplati et une valeur encodée en binaire.
Pour en extraire une sous-colonne de chemin, il suffit de parcourir toutes les lignes de cette colonne Map et d’essayer de trouver le chemin demandé ainsi que ses valeurs.
Dans les tables MergeTree, nous stockons les données dans des parts de données qui stockent tout sur disque (local ou distant). Les données sur disque peuvent être stockées différemment des données en mémoire.
Actuellement, il existe 3 sérialisations différentes de la structure de données partagée dans les parts de données MergeTree : map, map_with_buckets
et advanced.
La version de sérialisation est contrôlée par les paramètres MergeTree
object_shared_data_serialization_version
et object_shared_data_serialization_version_for_zero_level_parts
(une part de niveau zéro est la part créée lors de l’insertion de données dans la table ; lors des merges, les parts ont un niveau plus élevé).
Remarque : la modification de la sérialisation de la structure de données partagée n’est prise en charge que
pour v3, la version de sérialisation d’objet
Dans la version de sérialisation map, les données partagées sont sérialisées sous la forme d’une seule colonne de type Map(String, String), de la même manière qu’elles sont stockées en
mémoire. Pour lire une sous-colonne de chemin à partir de ce type de sérialisation, ClickHouse lit l’intégralité de la colonne Map et
extrait en mémoire le chemin demandé.
Cette sérialisation est efficace pour l’écriture des données et la lecture de l’ensemble de la colonne JSON, mais elle n’est pas efficace pour la lecture des sous-colonnes de chemins.
Dans la version de sérialisation map_with_buckets, les données partagées sont sérialisées en N colonnes (« buckets ») de type Map(String, String).
Chaque bucket ne contient qu’un sous-ensemble de chemins. Pour lire une sous-colonne de chemin à partir de ce type de sérialisation, ClickHouse
lit l’intégralité de la colonne Map depuis un seul bucket et extrait en mémoire le chemin demandé.
Cette sérialisation est moins efficace pour l’écriture des données et la lecture de l’intégralité de la colonne JSON, mais elle est plus efficace pour la lecture des sous-colonnes de chemins,
car elle lit uniquement les données des buckets requis.
Le nombre de buckets N est contrôlé par les paramètres MergeTree object_shared_data_buckets_for_compact_part (8 par défaut)
et object_shared_data_buckets_for_wide_part (32 par défaut).
La valeur maximale autorisée pour ces deux paramètres est de 256.
Dans la version de sérialisation advanced, les données partagées sont sérialisées dans une structure de données spéciale qui maximise les performances
de lecture des sous-colonnes de chemins en stockant des informations supplémentaires permettant de ne lire que les données des chemins demandés.
Cette sérialisation prend également en charge les buckets, de sorte que chaque bucket ne contient qu’un sous-ensemble de chemins.
Cette sérialisation est assez peu efficace en écriture (il n’est donc pas recommandé de l’utiliser pour les parts de niveau zéro) ; la lecture de l’intégralité de la colonne JSON est légèrement moins efficace qu’avec la sérialisation map, mais elle est très performante pour la lecture des sous-colonnes de chemins.
Remarque : comme cette structure de données stocke des informations supplémentaires, l’espace disque occupé est plus important avec cette sérialisation qu’avec les sérialisations
map et map_with_buckets.
Pour un aperçu plus détaillé des nouvelles sérialisations de données partagées et de leur implémentation, consultez le billet de blog.
Contrôle du nombre de chemins dynamiques dans JSON au sein des parts MergeTree
max_dynamic_paths dans la déclaration du type JSON.
Mais modifier max_dynamic_paths pour des colonnes existantes nécessite d’exécuter ALTER TABLE <table> MODIFY COLUMN <column> JSON(max_dynamic_paths=K), ce qui déclenchera une mutation en arrière-plan réécrivant toutes les parts existantes.
Une telle mutation peut être très lourde et affecter les performances du serveur jusqu’à sa fin. Pour éviter cela, vous pouvez utiliser ces 3 paramètres, qui peuvent vous aider à modifier la limite des chemins dynamiques dans les tables MergeTree pour les nouvelles parts de données :
merge_max_dynamic_subcolumns_in_wide_part- un paramètre MergeTree qui limite le nombre de sous-colonnes dynamiques pour chaque colonne JSON lors d’une fusion vers une part de données Wide.merge_max_dynamic_subcolumns_in_compact_part- un paramètre MergeTree qui limite le nombre de sous-colonnes dynamiques pour chaque colonne JSON lors d’une fusion vers une part de données Compact.max_dynamic_subcolumns_in_json_type_parsing- un paramètre de session qui limite le nombre de sous-colonnes dynamiques pour chaque colonne JSON lors de l’analyse des données JSON en colonne JSON.
max_dynamic_paths, même si les valeurs des paramètres décrits sont plus élevées.
Fonctions d’introspection
JSONAllPathsJSONAllPathsWithTypesJSONAllValuesJSONDynamicPathsJSONDynamicPathsWithTypesJSONSharedDataPathsJSONSharedDataPathsWithTypesdistinctDynamicTypesdistinctJSONPaths and distinctJSONPathsAndTypes
2020-01-01 :
Query
Response
Query
Response
ALTER MODIFY COLUMN pour le type JSON
JSON. À l’heure actuelle, seul ALTER à partir du type String est pris en charge.
Exemple
Query
Response
Lazy Type Hints (Expérimental)
Cette fonctionnalité est expérimentale et nécessite que le paramètre
allow_experimental_json_lazy_type_hints soit activé.ALTER TABLE ... MODIFY COLUMN, ClickHouse réécrit normalement toutes les parts de données afin de matérialiser les nouvelles indications de type. Pour les tables contenant de grandes quantités de données historiques (des centaines de téraoctets), cela peut être extrêmement coûteux.
Les Lazy type hints permettent d’ajouter des indications de type comme simple opération sur les métadonnées, sans réécrire les données existantes :
- Anciennes parts : les indications de type sont appliquées au moment de la requête par conversion de
Dynamicvers le type indiqué - Nouvelles parts : les indications de type sont matérialisées lors des opérations
INSERT - Fusions : les indications de type sont matérialisées lorsque les parts sont fusionnées
Activer les Lazy Type Hints
Exemple
Query
Response
Vérifier qu’aucune mutation n’a été effectuée
ALTER s’est exécuté sans mutation en consultant la table system.mutations :
Matérialisation des indications de type
- Attendre les fusions en arrière-plan : ClickHouse matérialisera automatiquement les indications de type lors de la fusion des parts
- Forcer la fusion : utilisez
OPTIMIZE TABLE test_lazy FINALpour fusionner immédiatement toutes les parts - Réécrire les parts : utilisez
ALTER TABLE test_lazy REWRITE PARTSpour réécrire les parts avec les nouvelles métadonnées
Limitations
- Cette fonctionnalité est expérimentale et peut évoluer dans de futures versions
- La conversion de types au moment de la requête peut entraîner un surcoût important en termes de performances par rapport aux types pré-matérialisés, en particulier pour les objets JSON volumineux
- Cette fonctionnalité s’applique uniquement lors de la modification de
typed_paths(indications de type) ; les autres paramètres JSON, commemax_dynamic_paths,SKIPouSKIP REGEXP, nécessitent toujours des mutations
Comparaison entre les valeurs du type JSON
Query
Response
Variant.
Index de saut de données pour JSON
JSON de trois façons :
- Index sur des sous-colonnes spécifiques — créez un index de saut standard sur un chemin JSON connu, comme pour une colonne classique. Cela indexe les valeurs de ce chemin.
- Index basés sur les chemins avec
JSONAllPaths— indexez l’ensemble des chemins présents dans chaque granule afin d’ignorer les granules qui ne peuvent pas contenir le chemin recherché. - Index basés sur les valeurs avec
JSONAllValues— indexez toutes les valeurs de tous les chemins JSON à l’aide d’un index textuel afin d’accélérer la recherche en texte intégral sur n’importe quelle sous-colonne JSON avec un seul index.
Index sur des sous-colonnes spécifiques
minmax, set, bloom_filter, tokenbf_v1, ngrambf_v1, etc.).
Il existe deux façons de faire référence à une sous-colonne JSON dans une expression d’index :
- Chemin typé déclaré dans l’indication de type JSON — accès direct par son nom :
json.a. - Chemin dynamique avec transtypage explicite — utilisez la syntaxe de transtypage
:::json.b::String.
json.a || json.b::String.
Exemple
Query
minmax sur la sous-colonne typée data.sensor_id restreint le balayage aux granules correspondants :
Query
Response
bloom_filter sur la sous-colonne data.location::String, obtenue par transtypage, fonctionne également :
Query
Response
Index basés sur les chemins avec JSONAllPaths
JSON à l’aide de la fonction JSONAllPaths.
Le fonctionnement est similaire à celui de la création de skip indexes sur des colonnes Map via mapKeys : l’index stocke l’ensemble des chemins JSON présents dans chaque granule et s’en sert pour ignorer les granules qui ne peuvent pas contenir le chemin recherché.
Types d’index pris en charge
JSONAllPaths peut être utilisé avec les types d’index de saut suivants :
bloom_filter— prend en chargeequals,inetIS NOT NULL.tokenbf_v1— prend en chargeequalsetIS NOT NULL.ngrambf_v1— prend en chargeequalsetIS NOT NULL.text(index inversé) — prend en chargeequals,inetIS NOT NULL.
Exemple
Query
EXPLAIN indexes = 1 pour vérifier que le skip index est utilisé. Lorsqu’un chemin n’existe que dans une seule part, l’index évite de lire l’autre part :
Query
Response
Query
Response
IS NOT NULL utilise également l’index — il ignore les granules où le chemin est absent (puisque la valeur serait NULL) :
Query
Response
Comment cela fonctionne
JSONAllPaths(json_column) produit un Array(String) contenant tous les chemins présents dans une valeur JSON.
L’index de saut stocke ces chaînes de chemin dans sa structure de données (filtre de Bloom ou index inversé).
Lorsqu’une requête filtre sur json.some.path, l’index vérifie si la chaîne "some.path" est présente dans l’index pour chaque granule et ignore les granules où elle est absente.
Sécurité en cas de chemins manquants
NULLpour le typeDynamic(par ex.json.path) et les sous-colonnes de typeNullable(par ex.json.path.:Int64) — les comparaisons avecNULLrenvoient toujours false, le skipping est donc sûr.- La valeur par défaut du type pour les expressions
CASTnonNullable(par ex.json.path::Int64produit0lorsque le chemin est absent) — le skipping n’est sûr que lorsque la valeur comparée diffère de la valeur par défaut. L’index gère automatiquement cette distinction.
Recherche en texte intégral avec JSONAllValues
JSONAllValues.
JSONAllValues renvoie toutes les valeurs d’une colonne JSON sous forme de Array(String), qui peut être indexé par un index de texte intégral.
Un seul index sur JSONAllValues(json_column) couvre tous les chemins JSON, ce qui permet d’effectuer une recherche en texte intégral sur n’importe quelle sous-colonne sans créer d’index distinct pour chaque chemin.
Voir Index basés sur les valeurs avec JSONAllValues dans la documentation des index de texte intégral pour plus de détails et d’exemples.
Conseils pour mieux utiliser le type JSON
JSON et d’y charger des données, tenez compte des conseils suivants :
- Analysez vos données et indiquez autant de chemins que possible, avec leurs types. Cela rendra le stockage et la lecture bien plus efficaces.
- Réfléchissez aux chemins dont vous aurez besoin et à ceux dont vous n’aurez jamais besoin. Indiquez les chemins inutiles dans la section
SKIPet, si nécessaire, dans la sectionSKIP REGEXP. Cela améliorera le stockage. - Ne définissez pas le paramètre
max_dynamic_pathssur des valeurs trop élevées, car cela peut rendre le stockage et la lecture moins efficaces. Bien que cela dépende fortement des paramètres du système, comme la mémoire, le CPU, etc., une règle générale consiste à ne pas définirmax_dynamic_pathsau-delà de 10 000 pour le stockage sur le système de fichiers local et de 1024 pour le stockage sur le système de fichiers distant.