Optimisation des chaînes dans ClickHouse. Rapport Yandex

Le moteur de base de données analytique ClickHouse traite de nombreuses lignes différentes, consommant des ressources. Pour accélérer le système, de nouvelles optimisations sont constamment ajoutées. Le développeur de ClickHouse Nikolay Kochetov parle du type de données de chaîne, y compris le nouveau type, LowCardinality, et explique comment accélérer le travail avec les chaînes.


- Tout d'abord, voyons comment vous pouvez stocker des chaînes.



Nous avons des types de données de chaîne. La chaîne fonctionne bien par défaut, elle devrait être utilisée presque toujours. Il a une petite surcharge - 9 octets par ligne. Si nous voulons que la taille de la ligne soit fixe et connue à l'avance, il est préférable d'utiliser FixedString. Dans celui-ci, vous pouvez définir le nombre d'octets dont nous avons besoin, il est pratique pour les données telles que les adresses IP ou les fonctions de hachage.



Bien sûr, quelque chose ralentit parfois. Supposons que vous effectuez une requête sur une table. ClickHouse lit une quantité assez importante de données, disons, à une vitesse de 100 Go / s, avec peu de lignes en cours de traitement. Nous avons deux tableaux qui stockent presque les mêmes données. ClickHouse lit les données de la deuxième table à une vitesse plus élevée, mais elle lit trois fois moins de lignes par seconde.



Si nous regardons la taille des données compressées, elle sera presque égale. En fait, les mêmes données sont écrites dans les tableaux - le premier milliard de nombres - seulement dans la première colonne sont-elles écrites sous la forme UInt64, et dans la deuxième colonne dans String. Pour cette raison, la deuxième requête lit les données du disque plus longtemps et les décompresse.



Voici un autre exemple. Supposons qu'il existe un ensemble de lignes prédéterminé, il est limité à une constante de 1000 ou 10 000 et ne change presque jamais. Dans ce cas, le type de données Enum nous convient, dans ClickHouse il y en a deux - Enum8 et Enum16. En raison du stockage dans Enum, nous traitons rapidement les demandes.

ClickHouse a des accélérations pour GROUP BY, IN, DISTINCT et des optimisations pour certaines fonctions, par exemple, pour la comparaison avec une chaîne constante. Bien sûr, les nombres de la chaîne ne sont pas convertis, mais, au contraire, la chaîne constante est convertie en la valeur Enum. Après cela, tout est rapidement comparé.

Mais il y a aussi des inconvénients. Même si nous connaissons l'ensemble exact de lignes, il doit parfois être renouvelé. Une nouvelle ligne est arrivée - nous devons faire ALTER.



ALTER pour Enum dans ClickHouse est implémenté de manière optimale. Nous n'écrase pas les données sur le disque, mais ALTER peut ralentir en raison du fait que les structures Enum sont stockées dans le schéma de la table elle-même. Par conséquent, nous devons attendre les demandes de lecture de la table, par exemple.

La question est, peut-on faire mieux? Peut-être oui. Vous pouvez enregistrer la structure Enum non pas dans le schéma de table, mais dans ZooKeeper. Cependant, des problèmes de synchronisation peuvent survenir. Par exemple, une réplique a reçu des données, l'autre non, et si elle a un ancien Enum, alors quelque chose va se casser. (Dans ClickHouse, nous avons presque terminé les demandes ALTER non bloquantes. Lorsque nous les aurons complètement terminées, nous n'aurons pas à attendre les demandes de lecture.)



Pour ne pas jouer avec ALTER Enum, vous pouvez utiliser des dictionnaires ClickHouse externes. Permettez-moi de vous rappeler qu'il s'agit d'une structure de données de valeur-clé à l'intérieur de ClickHouse, avec laquelle vous pouvez obtenir des données à partir de sources externes, par exemple, à partir de tables MySQL.

Dans le dictionnaire ClickHouse, nous stockons de nombreuses lignes différentes, et dans le tableau leurs identifiants sous forme de nombres. Si nous devons obtenir une chaîne, nous appelons la fonction dictGet et travaillons avec. Après cela, nous ne devrions pas faire ALTER. Pour ajouter quelque chose à Enum, nous l'insérons dans la même table MySQL.

Mais il y a d'autres problèmes. Tout d'abord, la syntaxe maladroite. Si nous voulons obtenir une chaîne, nous devons appeler dictGet. Deuxièmement, le manque de quelques optimisations. La comparaison avec la chaîne constante pour les dictionnaires n'est pas non plus rapide.

Il peut encore y avoir des problèmes avec la mise à jour. Supposons que nous ayons demandé une ligne dans le dictionnaire de cache, mais qu'elle ne soit pas entrée dans le cache. Ensuite, nous devons attendre que les données soient chargées à partir d'une source externe.



Un inconvénient commun des deux méthodes est que nous stockons toutes les clés au même endroit et les synchronisons. Alors pourquoi ne pas stocker les dictionnaires localement? Pas de synchronisation - pas de problème. Vous pouvez stocker le dictionnaire localement dans un morceau sur le disque. Autrement dit, nous avons inséré, enregistré un dictionnaire. Si nous travaillons avec des données en mémoire, nous pouvons écrire un dictionnaire soit dans un bloc de données, soit dans un morceau de colonne, soit dans un cache pour accélérer les calculs.

Encodage de chaînes de vocabulaire


Nous sommes donc arrivés à la création d'un nouveau type de données dans ClickHouse - LowCardinality. Il s'agit d'un format de stockage des données: comment elles sont écrites sur le disque et comment elles sont lues, comment elles sont présentées en mémoire et le schéma de leur traitement.



Il y a deux colonnes sur la diapositive. À droite, les chaînes sont stockées de manière standard dans le type String. On peut voir qu'il s'agit d'une sorte de modèle de téléphone mobile. À gauche, il y a exactement la même colonne, uniquement dans le type LowCardinality. Il se compose d'un dictionnaire avec de nombreuses lignes différentes (lignes de la colonne de droite) et d'une liste de positions (numéros de ligne).

En utilisant ces deux structures, vous pouvez restaurer la colonne d'origine. Il existe également un index inverse inversé - une table de hachage qui vous aide à trouver la position dans le dictionnaire par ligne. Il est nécessaire d'accélérer certaines requêtes. Par exemple, si nous voulons comparer, recherchez une ligne dans notre colonne ou fusionnez-les.

LowCardinality est un type de données paramétrique. Il peut s'agir d'un nombre, ou de quelque chose qui est stocké sous forme de nombre, ou d'une chaîne, ou Nullable à partir d'eux.



La particularité de LowCardinality est qu'elle peut être enregistrée pour certaines fonctions. Un exemple de demande est présenté sur la diapositive. Dans la première ligne, j'ai créé une colonne de type LowCardinality from String, nommée S. Puis j'ai demandé son nom - ClickHouse a dit que c'était LowCardinality from String. D'accord.

La troisième ligne est presque la même, seulement nous avons appelé la fonction de longueur. Dans ClickHouse, la fonction de longueur renvoie le type de données UInt64. Mais nous avons obtenu LowCardinality de UInt64. À quoi ça sert?



Les noms des téléphones portables ont été stockés dans le dictionnaire, nous avons appliqué la fonction de longueur. Maintenant, nous avons un dictionnaire similaire, composé uniquement de nombres, ce sont les longueurs de chaînes. La colonne avec les positions n'a pas changé. En conséquence, nous avons traité moins de données, enregistrées sur le temps de demande.

Il peut y avoir d'autres optimisations, comme l'ajout d'un cache simple. Lors du calcul de la valeur d'une fonction, vous pouvez vous en souvenir et la rendre identique, ne pas recalculer.

L'optimisation de GROUP BY peut également être effectuée, car notre colonne avec le dictionnaire est déjà partiellement agrégée - nous pouvons calculer rapidement la valeur des fonctions de hachage et trouver approximativement le compartiment où placer la ligne suivante. Vous pouvez également spécialiser certaines fonctions d'agrégation, par exemple uniq, car vous ne pouvez lui envoyer qu'un dictionnaire et laisser les positions intactes - de cette façon, tout fonctionnera plus rapidement. Les deux premières optimisations que nous avons déjà ajoutées à ClickHouse.



Mais que se passe-t-il si nous créons une colonne avec notre type de données et y insérons de nombreuses lignes différentes incorrectes? Notre mémoire est-elle pleine? Non, il existe deux paramètres spéciaux pour cela dans ClickHouse. Le premier est low_cardinality_max_dictionary_size. Il s'agit de la taille maximale d'un dictionnaire qui peut être écrit sur le disque. L'insertion se produit comme suit: lorsque nous insérons les données, un flux de lignes nous parvient, à partir de celles-ci, nous formons un grand dictionnaire général. Si le dictionnaire devient plus grand que la valeur du paramètre, nous écrivons le dictionnaire actuel sur le disque et le reste des lignes quelque part sur le côté, à côté des index. Par conséquent, nous ne recompterons jamais un grand dictionnaire et n'obtiendrons aucun problème de mémoire.

Le deuxième paramètre est appelé low_cardinality_use_single_dictionary_for_part. Imaginez que dans le schéma précédent, lorsque nous avons inséré les données, notre dictionnaire était plein et nous l'avons écrit sur le disque. La question se pose, pourquoi ne pas former maintenant un autre exactement le même dictionnaire?

Quand il déborde, nous l'écrirons à nouveau sur le disque et commencerons à en former un troisième. Ce paramètre désactive simplement cette fonctionnalité par défaut.

En fait, de nombreux dictionnaires peuvent être utiles si nous voulons insérer un ensemble de lignes, mais des «ordures» insérées accidentellement. Disons que nous avons d'abord inséré les mauvaises lignes, puis nous avons inséré les bonnes. Ensuite, le dictionnaire sera divisé en plusieurs petits dictionnaires. Certains d'entre eux seront avec des ordures, mais ces derniers seront avec de bonnes lignes. Et si nous lisons, disons, seulement le dernier culot, alors tout fonctionnera aussi rapidement.



Avant de parler des avantages de LowCardinality, je dirai tout de suite qu'il est peu probable que nous réalisions une réduction des données sur le disque (bien que cela puisse arriver), car ClickHouse compresse les données. Il existe une option par défaut - LZ4. Vous pouvez également effectuer une compression à l'aide de ZSTD. Mais les deux algorithmes implémentent déjà la compression du dictionnaire, donc notre dictionnaire ClickHouse externe n'aidera pas beaucoup.

Afin de ne pas être infondé, j'ai pris des données de la métrique - String, LowCardinality (String) et Enum - et les ai enregistrées dans différents types de données. Il s'est avéré trois colonnes, où un milliard de lignes sont écrites. La première colonne, CodePage, a un total de 62 valeurs. Et vous pouvez voir que dans LowCardinality (String), ils les ont mieux pressés. La chaîne est un peu pire, mais cela est probablement dû au fait que les chaînes sont courtes, nous stockons leurs longueurs, elles prennent beaucoup d'espace et ne compressent pas bien.

Si vous prenez PhoneModel, il y en a plus de 48 000 et il n'y a presque aucune différence entre String et LowCardinality (String). Pour l'URL, nous avons également économisé seulement 2 Go - je pense que vous ne devriez pas vous fier à cela.

Estimation de la vitesse de travail



Lien de la diapositive

Maintenant, évaluons la vitesse de travail. Pour l'évaluer, j'ai utilisé un ensemble de données décrivant les trajets en taxi à New York. Ilest disponiblesur GitHub. Il compte un peu plus d'un milliard de voyages. Il indique l'emplacement, les heures de début et de fin du voyage, le mode de paiement, le nombre de passagers et même le type de taxi - vert, jaune et Uber.



J'ai fait la première demande assez simple - j'ai demandé où les taxis sont le plus souvent commandés. Pour ce faire, vous devez prendre l'emplacement d'où vous avez commandé, faire GROUP BY dessus et calculer la fonction de comptage. Ici ClickHouse donne quelque chose.



Pour mesurer la vitesse de traitement des requêtes, j'ai créé trois tables avec les mêmes données, mais j'ai utilisé trois types de données différents pour notre emplacement de départ - String, LowCardinality et Enum. LowCardinality et Enum sont cinq fois plus rapides que String. Enum est plus rapide car il fonctionne avec des nombres. LowCardinality - car l'optimisation GROUP BY est implémentée.



Compliquons la demande - demandez où se trouve le parc le plus populaire de New York. Encore une fois, nous mesurerons cela en fonction de l'endroit où le taxi est le plus souvent commandé, mais en même temps, nous filtrerons uniquement les endroits où il y a le mot «parc». Ajoutez également une fonction similaire.



Nous regardons l'heure - nous voyons qu'Enum a soudainement commencé à ralentir. Et cela fonctionne encore plus lentement que le type de données String standard. En effet, la fonction similaire n'est pas du tout optimisée pour Enum. Nous devons convertir nos lignes d'Enum en lignes régulières - nous faisons plus de travail. LowCardinality (String) n'est également pas optimisé par défaut, mais comme il fonctionne sur le dictionnaire, la requête est donc plus rapide que String.

Il y a un problème plus global avec Enum. Si nous voulons l'optimiser, nous devons le faire à chaque endroit du code. Supposons que nous ayons écrit une nouvelle fonction - vous devez absolument trouver des optimisations pour Enum. Et dans LowCardinality, tout est optimisé par défaut.



Regardons la dernière requête, plus artificielle. Nous calculerons simplement la fonction de hachage à partir de notre emplacement. La fonction de hachage est une requête assez lente, elle prend beaucoup de temps, donc tout ralentira trois fois.



LowCardinality est encore plus rapide, bien qu'il n'y ait pas de filtrage. Cela est dû au fait que nos fonctions ne fonctionnent que sur le dictionnaire. La fonction de calcul de hachage a un argument - elle peut traiter moins de données et peut également renvoyer LowCardinality.



Notre plan global est d'atteindre une vitesse qui n'est pas inférieure à celle de String dans tous les cas, et de sauver l'accélération. Et peut-être que nous remplacerons un jour String par LowCardinality, vous mettrez à jour ClickHouse, et tout fonctionnera pour vous un peu plus rapidement.

All Articles