2 points par GN⁺ 2024-10-23 | Aucun commentaire pour le moment. | Partager sur WhatsApp
  • ClickHouse introduit un nouveau type JSON qui place les valeurs de chaque chemin JSON dans un véritable stockage orienté colonnes afin d’éviter le goulot d’étranglement consistant à insérer les documents JSON comme chaînes puis à les parser à chaque fois
  • Le cœur de l’implémentation repose sur les types Variant et Dynamic ; même si un même chemin JSON contient des types différents comme entier, chaîne ou tableau, ils ne sont pas fusionnés de force vers un plus petit type commun
  • La valeur par défaut de max_dynamic_paths est 1024 et celle de max_dynamic_types est 32, ce qui limite le nombre de sous-colonnes et de fichiers par type afin de maîtriser l’augmentation des descripteurs de fichiers et du coût des merges
  • Des indications de type, SKIP et SKIP REGEXP permettent d’ajuster la manière dont chaque chemin est stocké, et les valeurs peuvent être lues via une syntaxe de sous-colonne comme C.a.b
  • Le nouveau type vise à remplacer Object('json'), désormais obsolète, et la feuille de route inclut encore des améliorations pour utiliser les chemins de clés JSON dans la clé primaire ou les index de data-skipping

Le défi d’adapter JSON au stockage en colonnes

  • JSON est utilisé comme format commun pour traiter des données semi-structurées et non structurées dans les logs, l’observabilité, le streaming de données en temps réel, le stockage des applications mobiles et les pipelines de machine learning
  • ClickHouse est une véritable base de données orientée colonnes qui stocke une table comme un ensemble de fichiers de données colonnaires sur disque afin d’effectuer compression et filtrage/agrégation vectorisés
  • Pour obtenir les mêmes performances avec JSON, il faut stocker les valeurs de chaque chemin JSON unique comme des colonnes, au lieu de stocker les documents dans une colonne de chaînes puis de les parser plus tard

Les quatre contraintes traitées par le nouveau type JSON

  • Stockage en colonnes par chemin

    • Les valeurs de chaque chemin JSON doivent elles aussi pouvoir être compressées comme des colonnes ordinaires, par exemple numériques, puis filtrées et agrégées de manière vectorisée
  • Types qui changent dynamiquement

    • Un même chemin JSON a peut contenir des types différents comme entier, réel ou tableau
    • ClickHouse ne peut pas le savoir à l’avance, et les types peuvent être incompatibles entre eux ; les fusionner vers un plus petit type commun pourrait donc entraîner une perte d’information
  • Éviter l’explosion du nombre de fichiers de colonnes

    • Si chaque nouveau chemin JSON crée un nouveau fichier de colonne, le nombre de fichiers sur disque explose avec des données ayant beaucoup de clés uniques
    • Les descripteurs de fichiers consomment de la mémoire, et un trop grand nombre de fichiers à traiter affecte aussi les performances des merges
  • Stockage dense des clés clairsemées

    • Lorsqu’il y a beaucoup de clés JSON uniques mais clairsemées, il ne faut pas répéter NULL ou une valeur par défaut sur chaque ligne où la valeur est absente
    • Seules les valeurs réelles doivent être stockées densément afin de rester scalable à l’échelle du pétaoctet

Le type Variant : une base qui ne force pas l’unification des types

  • Le type de données Variant est une fonctionnalité indépendante, utilisable séparément de JSON, qui permet de stocker et lire dans une même colonne de table des valeurs de types différents
  • Les colonnes ClickHouse classiques ont un type fixe, et les valeurs insérées doivent correspondre à ce type ou être converties implicitement
    • Une colonne Nullable utilise un fichier de masque NULL en plus du fichier des valeurs
    • Array stocke la taille des tableaux dans un fichier séparé, ce qui permet de calculer les offsets
  • Une colonne Variant stocke, dans des sous-colonnes par type, les valeurs ayant le même type concret
    • Exemple : toutes les valeurs Int64 sont stockées dans C.Int64.bin, toutes les valeurs String dans C.String.bin
  • Le type utilisé sur chaque ligne est suivi par une colonne discriminante UInt8
    • La valeur du discriminant correspond à l’index dans une liste triée de noms de types
    • Le discriminant 255 est réservé à NULL
    • En raison de cette conception, Variant peut avoir au maximum 255 types concrets
  • Les fichiers de données par type utilisent une structure de stockage dense qui ne contient que les lignes ayant une valeur
    • Les fichiers par type ne stockent pas de valeurs NULL
    • Pour retrouver la position de ligne dans le vrai fichier de type à partir de la ligne du discriminant, une colonne d’offsets UInt64 en mémoire est utilisée
    • Cet offset n’est pas stocké sur disque et peut être généré à la volée depuis le fichier de colonne du discriminant
  • Variant prend en charge un niveau d’imbrication arbitraire
    • Variant(T1, T2) et Variant(T2, T1) ont le même sens du point de vue de l’ordre des types
    • Il est possible d’imbriquer un Variant dans un autre Variant
  • Les valeurs d’un type imbriqué spécifique se lisent en ajoutant le nom du type comme sous-colonne
    • Exemple : C.Int64

Le type Dynamic : stocker sans connaître à l’avance la liste des types

  • Le type Dynamic est une fonctionnalité indépendante implémentée au-dessus de Variant, et peut être utilisé en dehors du contexte JSON
  • Dynamic ajoute deux capacités à Variant
    • stocker dans une même colonne des valeurs de types arbitraires sans avoir à définir à l’avance la liste des types
    • limiter le nombre de types stockés dans des fichiers de données de colonnes séparés
  • Le mode de stockage interne est identique à celui de Variant, mais avec un fichier supplémentaire C.dynamic_structure.bin
    • Ce fichier contient la liste des types stockés en sous-colonnes ainsi que des statistiques sur la taille des fichiers de données par type
    • Ces métadonnées sont utilisées pour la lecture des sous-colonnes et le merge des data parts
  • Dynamic(max_types=N) permet de limiter le nombre de types stockés dans des fichiers séparés
    • 0 <= N < 255
    • La valeur par défaut est 32
  • Une fois la limite atteinte, les valeurs des autres types sont stockées dans un unique fichier de colonne comme C.SharedVariant.bin
    • Le type de ce fichier est String
    • Chaque ligne contient une valeur chaîne suivant la structure <binary_encoded_data_type><binary_value>
    • Cela permet de stocker puis relire dans un seul fichier de colonne des valeurs de plusieurs types
  • Dynamic permet lui aussi, comme Variant, de lire les valeurs d’un type spécifique via des sous-colonnes nommées par type
    • Exemple : C.Int64

Déclaration du type JSON et structure de stockage

  • Le nouveau type JSON permet de stocker des objets JSON de structure arbitraire et de lire chaque valeur JSON sous forme de sous-colonne basée sur le chemin
  • La déclaration du type peut inclure des paramètres optionnels et des indications
<column_name> JSON(
    max_dynamic_paths=N,
    max_dynamic_types=M,
    some.path TypeName,
    SKIP path.to.skip,
    SKIP REGEXP 'paths_regexp')
  • max_dynamic_paths
    • La valeur par défaut est 1024
    • Définit le nombre de chemins de clés JSON stockés dans des sous-colonnes distinctes
    • Les chemins dépassant cette limite sont stockés ensemble dans une sous-colonne unique de structure spéciale
  • max_dynamic_types
    • La valeur par défaut est 32
    • La plage de valeurs va de 0 à 254
    • Définit le nombre de types de données stockés dans des fichiers de données de colonnes distincts pour une même colonne de chemin de clé JSON
    • Les nouveaux types dépassant cette limite sont stockés ensemble dans un unique fichier spécial de données de colonne
  • some.path TypeName
    • Il s’agit d’une indication de type pour un chemin JSON spécifique
    • Ce chemin est toujours stocké comme sous-colonne du type indiqué, ce qui garantit les performances
  • SKIP path.to.skip
    • Ignore un chemin JSON spécifique pendant le parsing
    • Ce chemin n’est pas stocké dans la colonne JSON
    • Si le chemin indiqué correspond à un objet JSON imbriqué, l’objet imbriqué entier est ignoré
  • SKIP REGEXP 'path_regexp'
    • Ignore pendant le parsing les chemins correspondant à l’expression régulière
    • Les chemins correspondants ne sont pas stockés dans la colonne JSON

Comment les chemins JSON sont lus comme des colonnes

  • La valeur de chaque chemin leaf unique d’une colonne JSON est stockée sur disque de l’une des deux manières suivantes
    • Les chemins avec indication de type sont stockés dans des fichiers de données de colonnes classiques
    • Les chemins dont le type peut changer dynamiquement sont stockés dans des sous-colonnes Dynamic
  • Le type JSON utilise un fichier spécial appelé object_structure
    • Il contient les métadonnées des chemins dynamiques
    • Il contient les statistiques des valeurs non nulles de chaque chemin dynamique
    • Il est utilisé pour la lecture des sous-colonnes et le merge des data parts
  • L’explosion du nombre de fichiers de colonnes est maîtrisée par une limite à deux niveaux
    • max_dynamic_types limite le nombre de types stockés dans des fichiers séparés au sein d’un même chemin de clé JSON
    • max_dynamic_paths limite le nombre de chemins de clés JSON stockés dans des sous-colonnes distinctes
  • Les chemins JSON dynamiques supplémentaires qui dépassent la limite max_dynamic_paths sont stockés comme shared data
    • Exemples de fichiers : C.object_shared_data.size0.bin, C.object_shared_data.paths.bin, C.object_shared_data.values.bin
    • object_shared_data.values est de type String
    • Chaque entrée suit la structure <binary_encoded_data_type><binary_value>
  • Des statistiques supplémentaires sont aussi stockées dans object_structure.bin pour les shared data
    • À l’heure actuelle, les statistiques de valeurs non nulles sont conservées pour les 10 000 premiers chemins présents dans la colonne shared data

Syntaxe des chemins JSON et objets imbriqués

  • Le type JSON permet de lire la valeur leaf de chaque chemin comme une sous-colonne basée sur le nom du chemin
    • Exemple : C.a.b
  • La valeur d’un chemin sans indication de type a toujours le type Dynamic
    • Exemple : le type de C.a.d est Dynamic
  • Les sous-colonnes de sous-types du type Dynamic se lisent avec une syntaxe JSON spéciale
    • Exemple : C.a.d.:Int64
  • Les objets JSON imbriqués peuvent être lus comme des sous-colonnes de type JSON avec la syntaxe JSON_column.^some.path
    • Exemple : C.^a
  • Pour l’instant, la syntaxe avec point ne lit pas les objets imbriqués pour des raisons de performance
    • La structure de stockage actuelle est efficace pour lire des valeurs littérales chemin par chemin
    • Lire un sous-objet complet pour chaque chemin peut nécessiter davantage de lecture de données et donc être plus lent
    • La syntaxe .^ est nécessaire pour retourner des objets
    • ClickHouse prévoit d’unifier les deux syntaxes .

Sérialisation compacte du discriminant

  • De nombreux chemins JSON dynamiques peuvent avoir la même majorité de types de valeurs
  • Lorsqu’il existe beaucoup de chemins JSON uniques mais clairsemés, le fichier de discriminant de chaque chemin contient surtout 255, c’est-à-dire la valeur NULL
  • Ces fichiers se compressent bien, mais restent malgré tout redondants lorsque toutes les valeurs d’une ligne sont identiques
  • ClickHouse a implémenté un format compact pour la sérialisation des discriminants
    • Au lieu d’écrire systématiquement toutes les valeurs de discriminant UInt8, si tous les discriminants d’un granule donné sont identiques, seules 3 valeurs sont sérialisées
    • un marqueur de format compact de granule
    • un marqueur du nombre de valeurs dans ce granule
    • la valeur du discriminant
  • Cette optimisation est contrôlée par le réglage MergeTree use_compact_variant_discriminators_serialization
    • Il est activé par défaut
    • Dans certains cas, cela permet de ne stocker que 3 valeurs au lieu des 8192 valeurs habituelles avec l’index granularity standard

État de la release et prochaines étapes

  • Le nouveau type JSON est conçu pour remplacer le type Object('json'), désormais obsolète
  • L’implémentation est disponible comme fonctionnalité experimental dans la release ClickHouse 24.08 à des fins de test
  • La feuille de route JSON inclut des améliorations permettant d’utiliser les chemins de clés JSON dans la clé primaire d’une table ou dans des index de data-skipping
  • Des composants comme Variant et Dynamic servent aussi de base à la prise en charge d’autres types semi-structurés, comme XML ou YAML, au-delà de JSON
  • Les utilisateurs de ClickHouse Cloud qui souhaitent tester le nouveau type de données JSON doivent contacter l’équipe support ClickHouse pour demander un accès en private preview

Aucun commentaire pour le moment.

Aucun commentaire pour le moment.