Introduction
- en quoi l’indexation dans ClickHouse diffère de celle des systèmes traditionnels de gestion de bases de données relationnelles
- comment ClickHouse construit et utilise l’index primaire sparse d’une table
- quelles sont quelques bonnes pratiques d’indexation dans ClickHouse
Jeu de données
- Nous utiliserons un sous-ensemble de 8,87 millions de lignes (événements) issu de cet exemple de jeu de données.
- La taille des données non compressées est de 8,87 millions d’événements et d’environ 700 Mo. Une fois stockées dans ClickHouse, elles sont compressées à 200 Mo.
- Dans notre sous-ensemble, chaque ligne contient trois colonnes correspondant à un internaute (colonne
UserID) ayant cliqué sur une URL (colonneURL) à un moment précis (colonneEventTime).
- « Quelles sont les 10 URL les plus cliquées pour un utilisateur donné ? »
- « Quels sont les 10 utilisateurs qui ont le plus souvent cliqué sur une URL donnée ? »
- « Quels sont les moments les plus fréquents (par ex. les jours de la semaine) où un utilisateur clique sur une URL donnée ? »
Machine de test
Un parcours complet de la table
Conception des index dans ClickHouse
Une conception d’index pour des volumes de données massifs
B(+)-Tree a une complexité temporelle moyenne de O(log n) ; plus précisément, log_b n = log_2 n / log_2 b où b est le facteur de branchement du B(+)-Tree et n le nombre de lignes indexées. Comme b est généralement compris entre quelques centaines et quelques milliers, les B(+)-Tree sont des structures très peu profondes, et peu d’accès disque sont nécessaires pour localiser les enregistrements. Avec 8,87 millions de lignes et un facteur de branchement de 1000, 2,3 accès disque sont nécessaires en moyenne. Cette capacité a cependant un coût : une surcharge supplémentaire en disque et en mémoire, des coûts d’insertion plus élevés lors de l’ajout de nouvelles lignes dans la table et de nouvelles entrées dans l’index, et parfois un rééquilibrage du B-Tree.
Compte tenu des défis associés aux index B-Tree, les moteurs de table de ClickHouse utilisent une approche différente. La famille de moteurs MergeTree de ClickHouse a été conçue et optimisée pour gérer des volumes massifs de données. Ces tables sont conçues pour recevoir des millions d’insertions de lignes par seconde et stocker de très grands volumes de données (des centaines de pétaoctets). Les données sont rapidement écrites dans une table part par part, selon des règles de fusion appliquées en arrière-plan. Dans ClickHouse, chaque part possède son propre index primaire. Lorsque des parts fusionnent, leurs index primaires fusionnent également. À l’échelle très importante pour laquelle ClickHouse a été conçu, il est primordial d’être extrêmement efficace en termes de disque et de mémoire. Par conséquent, au lieu d’indexer chaque ligne, l’index primaire d’une part contient une entrée d’index (appelée « mark ») par groupe de lignes (appelé « granule ») : cette technique est appelée index clairsemé.
L’indexation clairsemée est possible parce que ClickHouse stocke sur disque les lignes d’une part dans l’ordre des colonnes de clé primaire. Au lieu de localiser directement des lignes individuelles (comme le ferait un index basé sur un B-Tree), l’index primaire sparse lui permet d’identifier rapidement (grâce à une recherche binaire dans les entrées d’index) les groupes de lignes susceptibles de correspondre à la requête. Les groupes de lignes potentiellement correspondantes ainsi localisés (granules) sont ensuite transmis en flux, en parallèle, au moteur ClickHouse afin de trouver les correspondances. Cette conception d’index permet de conserver un index primaire compact (il peut, et doit, tenir entièrement en mémoire principale), tout en accélérant considérablement les temps d’exécution des requêtes, en particulier pour les requêtes par plage typiques des cas d’usage analytiques.
La suite illustre en détail comment ClickHouse construit et utilise son index primaire sparse. Plus loin dans l’article, nous aborderons quelques bonnes pratiques pour choisir, supprimer et ordonner les colonnes de table utilisées pour construire l’index (colonnes de clé primaire).
Une table avec une clé primaire
Détails de l’instruction DDL
Détails de l’instruction DDL
Afin de simplifier les explications plus loin dans ce guide, ainsi que de rendre les schémas et les résultats reproductibles, l’instruction DDL :
- Définit une clé de tri composée pour la table au moyen d’une clause
ORDER BY. - Contrôle explicitement le nombre d’entrées d’index de l’index primaire au moyen des paramètres suivants :
index_granularity: explicitement défini sur sa valeur par défaut, 8192. Cela signifie que pour chaque groupe de 8192 lignes, l’index primaire comporte une entrée d’index. Par exemple, si la table contient 16384 lignes, l’index comporte deux entrées d’index.index_granularity_bytes: défini sur 0 afin de désactiver la granularité d’index adaptative. La granularité d’index adaptative signifie que ClickHouse crée automatiquement une entrée d’index pour un groupe de n lignes si l’une des conditions suivantes est remplie :- Si
nest inférieur à 8192 et que la taille combinée des données de ligne pour cesnlignes est supérieure ou égale à 10 Mo (la valeur par défaut deindex_granularity_bytes). - Si la taille combinée des données de ligne pour
nlignes est inférieure à 10 Mo, mais quenvaut 8192.
- Si
compress_primary_key: défini sur 0 pour désactiver la compression de l’index primaire. Cela nous permettra, si nécessaire, d’en inspecter le contenu plus tard.
Insérez ensuite les données :
Puis optimisez la table :
Nous pouvons utiliser la requête suivante pour obtenir des métadonnées sur notre table :
- Les données de la table sont stockées au format wide dans un répertoire spécifique sur le disque, ce qui signifie qu’il y aura un fichier de données (et un fichier de marks) par colonne de la table dans ce répertoire.
- La table contient 8,87 millions de lignes.
- La taille non compressée de l’ensemble des lignes est de 733,28 Mo.
- La taille compressée sur le disque de l’ensemble des lignes est de 206,94 Mo.
- La table possède un index primaire avec 1083 entrées (appelées ‘marks’) et sa taille est de 96,93 Ko.
- Au total, les données de la table, les fichiers de marks et le fichier d’index primaire occupent ensemble 207,07 Mo sur le disque.
Les données sont stockées sur disque dans l’ordre des colonnes de la clé primaire
- une clé primaire composite
(UserID, URL)et - une clé de tri composite
(UserID, URL, EventTime).
- Si nous avions spécifié uniquement la clé de tri, la clé primaire aurait alors été implicitement définie comme identique à la clé de tri.
- Pour optimiser l’utilisation de la mémoire, nous avons explicitement défini une clé primaire qui ne contient que les colonnes sur lesquelles nos requêtes filtrent. L’index primaire, basé sur la clé primaire, est entièrement chargé en mémoire principale.
- Pour conserver une cohérence dans les schémas du guide et maximiser le taux de compression, nous avons défini une clé de tri distincte qui inclut toutes les colonnes de la table (si, dans une colonne, des données similaires sont rapprochées, par exemple grâce au tri, elles seront mieux compressées).
- La clé primaire doit être un préfixe de la clé de tri si les deux sont spécifiées.
EventTime de la clé de tri).
EventTime.- dans la représentation sur disque, il y a un seul fichier de données (*.bin) par colonne de table, dans lequel toutes les valeurs de cette colonne sont stockées au format compressé, et
- les 8,87 millions de lignes sont stockées sur disque dans l’ordre lexicographique croissant des colonnes de la clé primaire (et des colonnes supplémentaires de la clé de tri), c’est-à-dire ici
- d’abord par
UserID, - puis par
URL, - et enfin par
EventTime:
- d’abord par
UserID.bin, URL.bin et EventTime.bin sont les fichiers de données sur disque dans lesquels sont stockées les valeurs des colonnes UserID, URL et EventTime.
- Comme la clé primaire définit l’ordre lexicographique des lignes sur disque, une table ne peut avoir qu’une seule clé primaire.
- Nous numérotons les lignes à partir de 0 afin de rester alignés sur le schéma de numérotation interne des lignes de ClickHouse, également utilisé dans les messages de journalisation.
Les données sont organisées en granules pour leur traitement en parallèle
index_granularity (défini sur sa valeur par défaut de 8192).
Les 8192 premières lignes (d’après l’ordre physique sur le disque) — et leurs valeurs de colonnes — appartiennent logiquement au granule 0, puis les 8192 lignes suivantes (et leurs valeurs de colonnes) appartiennent au granule 1, et ainsi de suite.
- Le dernier granule (granule 1082) « contient » moins de 8192 lignes.
- Nous avons mentionné au début de ce guide, dans « DDL Statement Details », que nous avons désactivé la granularité d’index adaptative (afin de simplifier les explications de ce guide et de rendre les schémas ainsi que les résultats reproductibles). Par conséquent, tous les granules (sauf le dernier) de notre table d’exemple ont la même taille.
- Pour les tables avec une granularité d’index adaptative (la granularité d’index est adaptative par défaut), la taille de certains granules peut être inférieure à 8192 lignes, selon la taille des données de ligne.
-
Nous avons marqué en orange certaines valeurs de colonnes de nos colonnes de clé primaire (
UserID,URL). Ces valeurs de colonnes marquées en orange sont les valeurs des colonnes de clé primaire de la première ligne de chaque granule. Comme nous le verrons ci-dessous, ces valeurs de colonnes marquées en orange constitueront les entrées de l’index primaire de la table. - Nous numérottons les granules à partir de 0 afin d’être alignés sur le schéma de numérotation interne de ClickHouse, également utilisé pour les messages de log.
L’index primaire comporte une entrée par granule
primary.idx), qui contient ce que l’on appelle des index marks numériques commençant à 0.
Le schéma ci-dessous montre que l’index stocke les valeurs des colonnes de clé primaire (les valeurs marquées en orange dans le schéma ci-dessus) de la première ligne de chaque granule.
Autrement dit, l’index primaire stocke les valeurs des colonnes de clé primaire de chaque 8192e ligne de la table (selon l’ordre physique des lignes défini par les colonnes de clé primaire).
Par exemple
- la première entrée d’index (« mark 0 » dans le schéma ci-dessous) stocke les valeurs des colonnes de clé de la première ligne du granule 0 dans le schéma ci-dessus,
- la deuxième entrée d’index (« mark 1 » dans le schéma ci-dessous) stocke les valeurs des colonnes de clé de la première ligne du granule 1 dans le schéma ci-dessus, et ainsi de suite.
- Pour les tables avec granularité d’index adaptative, une marque « finale » supplémentaire est également stockée dans l’index primaire, afin d’enregistrer les valeurs des colonnes de clé primaire de la dernière ligne de la table. Cependant, comme nous avons désactivé la granularité d’index adaptative (afin de simplifier les explications de ce guide et de rendre les schémas ainsi que les résultats reproductibles), l’index de notre table d’exemple n’inclut pas cette marque finale.
- Le fichier d’index primaire est entièrement chargé en mémoire vive. Si sa taille dépasse l’espace mémoire libre disponible, ClickHouse génère une erreur.
Inspection du contenu de l’index primaire
Inspection du contenu de l’index primaire
Sur un cluster ClickHouse autogéré, nous pouvons utiliser la fonction de table file pour inspecter le contenu de l’index primaire de notre table d’exemple.Pour cela, nous devons d’abord copier le fichier d’index primaire dans le user_files_path d’un nœud du cluster actif :
- Étape 1 : obtenir le chemin de la part qui contient le fichier d’index primaire
- Étape 2 : obtenir le user_files_path Le user_files_path par défaut sous Linux est
- Étape 3 : copier le fichier d’index primaire dans le user_files_path
SELECT path FROM system.parts WHERE table = 'hits_UserID_URL' AND active = 1renvoie /Users/tomschreiber/Clickhouse/store/85f/85f4ee68-6e28-4f08-98b1-7d8affa1d88c/all_1_9_4 sur la machine de test./var/lib/clickhouse/user_files/et sous Linux, vous pouvez vérifier s’il a été modifié : $ grep user_files_path /etc/clickhouse-server/config.xmlSur la machine de test, le chemin est /Users/tomschreiber/Clickhouse/user_files/cp /Users/tomschreiber/Clickhouse/store/85f/85f4ee68-6e28-4f08-98b1-7d8affa1d88c/all_1_9_4/primary.idx /Users/tomschreiber/Clickhouse/user_files/primary-hits_UserID_URL.idxNous pouvons maintenant inspecter le contenu de l’index primaire en SQL :
- Obtenir le nombre d’entrées
- Obtenir les deux premiers index marks
- Obtenir le dernier index mark
SELECT count( )<br/>FROM file('primary-hits_UserID_URL.idx', 'RowBinary', 'UserID UInt32, URL String');
renvoie 1083SELECT UserID, URL<br/>FROM file('primary-hits_UserID_URL.idx', 'RowBinary', 'UserID UInt32, URL String')<br/>LIMIT 0, 2;renvoie240923, http://showtopics.html%3...<br/> 4073710, http://mk.ru&pos=3_0SELECT UserID, URL FROM file('primary-hits_UserID_URL.idx', 'RowBinary', 'UserID UInt32, URL String')<br/>LIMIT 1082, 1;
renvoie
4292714039 │ http://sosyal-mansetleri...Cela correspond exactement à notre diagramme du contenu de l’index primaire pour notre table d’exemple :
-
Index marks de UserID :
Les valeurs
UserIDstockées dans l’index primaire sont triées par ordre croissant.
« mark 1 » dans le diagramme ci-dessus indique donc que les valeursUserIDde toutes les lignes de la table dans le granule 1, ainsi que dans tous les granules suivants, sont nécessairement supérieures ou égales à 4.073.710.
-
Marques d’index URL :
La cardinalité assez similaire des colonnes de clé primaire
UserIDetURLsignifie que les marques d’index de toutes les colonnes de clé après la première n’indiquent généralement une plage de données que tant que la valeur de la colonne de clé précédente reste la même pour toutes les lignes de la table, au moins dans le granule courant.
Par exemple, comme les valeursUserIDde la marque 0 et de la marque 1 sont différentes dans le diagramme ci-dessus, ClickHouse ne peut pas supposer que toutes les valeursURLde toutes les lignes de la table dans le granule 0 sont supérieures ou égales à'http://showtopics.html%3...'. En revanche, si les valeursUserIDde la marque 0 et de la marque 1 étaient identiques dans le diagramme ci-dessus (ce qui signifie que la valeurUserIDreste la même pour toutes les lignes de la table dans le granule 0), ClickHouse pourrait supposer que toutes les valeursURLde toutes les lignes de la table dans le granule 0 sont supérieures ou égales à'http://showtopics.html%3...'. Nous examinerons plus en détail, plus tard, les conséquences de cela sur les performances d’exécution des requêtes.
L’index primaire sert à sélectionner les granules
749927693. Cela nécessite 19 étapes, avec une complexité temporelle moyenne de O(log2 n) :
Détails du Trace Log
Détails du Trace Log
La marque 176 a été identifiée (la ‘found left boundary mark’ est inclusive, la ‘found right boundary mark’ est exclusive), et par conséquent, les 8192 lignes de la granule 176 (qui commence à la ligne 1.441.792 — nous le verrons plus loin dans ce guide) sont alors lues par ClickHouse afin de trouver les lignes réelles dont la valeur de la colonne UserID est 749927693.
Comme indiqué ci-dessus, ClickHouse utilise son index primaire sparse pour sélectionner rapidement (via une recherche binaire) les granules susceptibles de contenir des lignes correspondant à une requête. Il s’agit de la première étape (sélection des granules) de l’exécution d’une requête ClickHouse. Lors de la deuxième étape (lecture des données), ClickHouse localise les granules sélectionnées afin de transmettre en flux toutes leurs lignes au ClickHouse engine pour trouver les lignes qui correspondent réellement à la requête. Nous examinons cette deuxième étape plus en détail dans la section suivante.
Les fichiers de marks servent à localiser les granules
Détails de la sélection des granules
Détails de la sélection des granules
Le schéma ci-dessus montre que la marque 176 est la première entrée de l’index pour laquelle la valeur UserID minimale du granule 176 associé est inférieure à 749.927.693, tandis que la valeur UserID minimale du granule 177 pour la marque suivante (marque 177) est supérieure à cette valeur. Par conséquent, seul le granule 176 correspondant à la marque 176 peut potentiellement contenir des lignes dont la colonne UserID a la valeur 749.927.693.
UserID.mrk, URL.mrk et EventTime.mrk qui stockent les emplacements physiques des granules pour les colonnes UserID, URL et EventTime de la table.
Nous avons vu que l’index primaire est un fichier tableau plat non compressé (primary.idx), contenant des marques d’index numérotées à partir de 0.
De la même manière, un fichier de marks est lui aussi un fichier tableau plat non compressé (*.mrk) contenant des marques numérotées à partir de 0.
Une fois que ClickHouse a identifié et sélectionné la marque d’index d’un granule susceptible de contenir des lignes correspondant à une requête, il est possible d’effectuer une recherche positionnelle dans les fichiers de marks afin d’obtenir les emplacements physiques du granule.
Chaque entrée du fichier de marks pour une colonne donnée stocke deux emplacements sous forme de décalages :
- Le premier décalage (‘block_offset’ dans le schéma ci-dessus) localise le bloc dans le fichier de données de colonne compressées qui contient la version compressée du granule sélectionné. Ce bloc compressé peut contenir plusieurs granules compressés. Le bloc compressé localisé est décompressé en mémoire principale lors de la lecture.
- Le second décalage (‘granule_offset’ dans le schéma ci-dessus) fourni par le fichier de marks indique l’emplacement du granule dans les données du bloc décompressé.
- Pour les tables au format wide et sans granularité d’index adaptative, ClickHouse utilise des fichiers de marks
.mrkcomme illustré ci-dessus, qui contiennent des entrées avec deux adresses de 8 octets par entrée. Ces entrées correspondent aux emplacements physiques de granules qui ont tous la même taille.
-
Pour les tables au format wide avec granularité d’index adaptative, ClickHouse utilise des fichiers de marks
.mrk2, qui contiennent des entrées similaires à celles des fichiers de marks.mrk, mais avec une troisième valeur supplémentaire par entrée : le nombre de lignes du granule auquel l’entrée courante est associée. -
Pour les tables au format compact, ClickHouse utilise des fichiers de marks
.mrk3.
EventTime.Pour notre requête d’exemple, ClickHouse n’a besoin que des deux offsets d’emplacement physique pour le granule 176 dans le fichier de données UserID (UserID.bin) et des deux offsets d’emplacement physique pour le granule 176 dans le fichier de données URL (URL.bin).L’indirection fournie par les fichiers de marks évite de stocker directement dans l’index primaire des entrées correspondant aux emplacements physiques des 1083 granules des trois colonnes, ce qui évite de conserver en mémoire principale des données inutiles (et potentiellement jamais utilisées).Utiliser plusieurs index primaires
Les colonnes de clé secondaires peuvent être inefficaces… ou non
Nous utilisons une requête qui calcule les 10 utilisateurs ayant cliqué le plus souvent sur l’URL “http://public_search” :
Algorithme de recherche par exclusion générique
- une requête qui recherche des lignes avec la valeur URL = “W3”.
- une version abstraite de notre table hits avec des valeurs simplifiées pour UserID et URL.
- la même clé primaire composée (UserID, URL) pour l’index. Cela signifie que les lignes sont d’abord triées par valeurs UserID. Les lignes ayant la même valeur UserID sont ensuite triées par URL.
- une taille de granule de deux, c’est-à-dire que chaque granule contient deux lignes.
- La marque d’index 0, pour laquelle la valeur URL est inférieure à W3 et pour laquelle la valeur URL de la marque d’index directement suivante est également inférieure à W3, peut être exclue, car les marques 0 et 1 ont la même valeur UserID. Notez que cette condition préalable à l’exclusion garantit que le granule 0 est entièrement composé de valeurs UserID U1, de sorte que ClickHouse peut supposer que la valeur URL maximale du granule 0 est elle aussi inférieure à W3, et exclure ce granule.
- La marque d’index 1, pour laquelle la valeur URL est inférieure (ou égale) à W3 et pour laquelle la valeur URL de la marque d’index directement suivante est supérieure (ou égale) à W3, est sélectionnée, car cela signifie que le granule 1 peut éventuellement contenir des lignes avec l’URL W3.
- Les marques d’index 2 et 3, pour lesquelles la valeur URL est supérieure à W3, peuvent être exclues, puisque les marques d’index d’un index primaire stockent les valeurs des colonnes de clé de la première ligne de table pour chaque granule et que les lignes de table sont triées sur le disque selon les valeurs des colonnes de clé. Par conséquent, les granules 2 et 3 ne peuvent pas contenir la valeur URL W3.
Remarque sur l’index de saut de données
GRANULARITY 4 dans l’instruction ALTER TABLE ci-dessus) — les valeurs minimale et maximale de l’URL :
La première entrée d’index (‘mark 0’ dans le schéma ci-dessus) stocke les valeurs minimale et maximale de l’URL pour les lignes appartenant aux 4 premières granules de notre table.
La deuxième entrée d’index (‘mark 1’) stocke les valeurs minimale et maximale de l’URL pour les lignes appartenant aux 4 granules suivantes de notre table, et ainsi de suite.
(ClickHouse a également créé un fichier de marques spécial pour l’index de data skipping afin de repérer les groupes de granules associés aux marques d’index.)
Étant donné la cardinalité élevée, et similaire, de UserID et de l’URL, cet index secondaire de data skipping ne peut pas aider à exclure des granules lors de l’exécution de notre requête filtrant sur l’URL.
La valeur d’URL spécifique recherchée par la requête (c.-à-d. ‘http://public_search') se situe très probablement entre les valeurs minimale et maximale stockées par l’index pour chaque groupe de granules, ce qui force ClickHouse à sélectionner ce groupe de granules (car il peut contenir des lignes correspondant à la requête).
Nécessité d’utiliser plusieurs index primaires
Options pour créer des index primaires supplémentaires
UserID spécifique et celle qui filtre les lignes sur une URL spécifique — nous devons utiliser plusieurs index primaires en recourant à l’une des trois options suivantes :
- Créer une deuxième table avec une clé primaire différente.
- Créer une vue matérialisée sur notre table existante.
- Ajouter une projection à notre table existante.
Option 1 : Tables secondaires
UserIDs ne s’exécutera pas très efficacement avec la nouvelle table supplémentaire, car UserID est désormais la deuxième colonne clé de l’index primaire de cette table et, par conséquent, ClickHouse utilisera la recherche générique par exclusion pour la sélection des granules, ce qui est peu efficace lorsque UserID et URL ont une cardinalité élevée similaire.
Ouvrez le volet de détails pour plus d’informations.
La requête avec filtrage sur UserIDs a maintenant de mauvaises performances
La requête avec filtrage sur UserIDs a maintenant de mauvaises performances
UserIDs et les requêtes avec filtrage sur les URL :
Option 2 : Vues matérialisées
- nous modifions l’ordre des colonnes clés (par rapport à notre table d’origine) dans la clé primaire de la vue
- la vue matérialisée repose sur une table créée implicitement dont l’ordre des lignes et l’index primaire sont définis à partir de la clé primaire fournie
- la table créée implicitement apparaît dans le résultat de la requête
SHOW TABLESet son nom commence par.inner - il est également possible de créer d’abord explicitement la table sous-jacente d’une vue matérialisée, puis de faire pointer la vue vers cette table via la clause
TO [db].[table] - nous utilisons le mot-clé
POPULATEafin de remplir immédiatement la table créée implicitement avec les 8,87 millions de lignes de la table source hits_UserID_URL - si de nouvelles lignes sont insérées dans la table source hits_UserID_URL, elles sont alors automatiquement insérées elles aussi dans la table créée implicitement
- En pratique, la table créée implicitement a le même ordre de lignes et le même index primaire que la table secondaire que nous avons créée explicitement :
Option 3 : Projections
- la projection crée une table masquée dont l’ordre des lignes et l’index primaire reposent sur la clause
ORDER BYde la projection - la table masquée n’apparaît pas dans le résultat de la requête
SHOW TABLES - nous utilisons le mot-clé
MATERIALIZEafin de remplir immédiatement la table masquée avec l’ensemble des 8,87 millions de lignes de la table source hits_UserID_URL - si de nouvelles lignes sont insérées dans la table source hits_UserID_URL, elles sont alors automatiquement insérées elles aussi dans la table masquée
- une requête cible toujours (syntaxiquement) la table source hits_UserID_URL, mais si l’ordre des lignes et l’index primaire de la table masquée permettent une exécution plus efficace de la requête, alors cette table masquée sera utilisée à la place
- veuillez noter que les projections ne rendent pas plus efficaces les requêtes qui utilisent
ORDER BY, même si la clauseORDER BYcorrespond à celle de la projection (voir https://github.com/ClickHouse/ClickHouse/issues/47333) - En pratique, la table masquée créée implicitement a le même ordre de lignes et le même index primaire que la table secondaire que nous avons créée explicitement :
Résumé
Ordonner efficacement les colonnes de clé
- l’efficacité du filtrage sur les colonnes de clé secondaires dans les requêtes, et
- le taux de compression des fichiers de données de la table.
UserID) à une URL (colonne URL) a été marqué ou non comme trafic de bot (colonne IsRobot).
Nous utiliserons une clé primaire composite contenant les trois colonnes mentionnées ci-dessus, qui peut servir à accélérer des requêtes typiques de web analytics calculant :
- quelle proportion (en pourcentage) du trafic vers une URL donnée provient de bots, ou
- avec quel degré de certitude un utilisateur donné est (ou n’est pas) un bot (quel pourcentage du trafic de cet utilisateur est, ou n’est pas, considéré comme du trafic de bot)
clickhouse client :
URL et IsRobot ; par conséquent, l’ordre de ces colonnes dans une clé primaire composée est important, à la fois pour accélérer les requêtes qui filtrent sur ces colonnes et pour obtenir des taux de compression optimaux pour les fichiers de données des colonnes de la table.
Pour le démontrer, nous créons deux versions de table pour nos données d’analyse du trafic de bots :
- une table
hits_URL_UserID_IsRobotavec la clé primaire composée(URL, UserID, IsRobot), où nous ordonnons les colonnes de la clé par cardinalité décroissante - une table
hits_IsRobot_UserID_URLavec la clé primaire composée(IsRobot, UserID, URL), où nous ordonnons les colonnes de la clé par cardinalité croissante
hits_URL_UserID_IsRobot avec la clé primaire composée (URL, UserID, IsRobot) :
hits_IsRobot_UserID_URL avec la clé primaire composite (IsRobot, UserID, URL) :
Filtrage efficace sur les colonnes secondaires de la clé
UserID de la table où nous avons ordonné les colonnes de clé (URL, UserID, IsRobot) par cardinalité décroissante :
(IsRobot, UserID, URL) par cardinalité croissante :
Taux de compression optimal des fichiers de données
UserID entre les deux tables que nous avons créées ci-dessus :
UserID est nettement plus élevé pour la table dans laquelle nous avons ordonné les colonnes clés (IsRobot, UserID, URL) par cardinalité croissante.
Bien que les deux tables stockent exactement les mêmes données (nous avons inséré les mêmes 8,87 millions de lignes dans les deux tables), l’ordre des colonnes clés dans la clé primaire composite influe fortement sur l’espace disque requis par les données compressées dans les fichiers de données des colonnes de la table :
- dans la table
hits_URL_UserID_IsRobotavec la clé primaire composite(URL, UserID, IsRobot), où nous ordonnons les colonnes clés par cardinalité décroissante, le fichier de donnéesUserID.binoccupe 11.24 MiB d’espace disque - dans la table
hits_IsRobot_UserID_URLavec la clé primaire composite(IsRobot, UserID, URL), où nous ordonnons les colonnes clés par cardinalité croissante, le fichier de donnéesUserID.binn’occupe que 877.47 KiB d’espace disque
cl, puis les lignes ayant la même valeur cl sont ordonnées par leur valeur ch. Comme la première colonne clé, cl, a une faible cardinalité, il est probable que plusieurs lignes aient la même valeur cl. Par conséquent, il est également probable que les valeurs ch soient ordonnées (localement, c’est-à-dire pour les lignes ayant la même valeur cl).
Si, dans une colonne, des données similaires sont placées à proximité les unes des autres, par exemple grâce au tri, elles seront mieux compressées.
De manière générale, un algorithme de compression tire parti de la longueur des séquences de données (plus il traite de données, mieux c’est pour la compression)
et de la localité (plus les données se ressemblent, meilleur est le taux de compression).
À l’inverse du schéma ci-dessus, le schéma ci-dessous représente l’ordre des lignes sur disque pour une clé primaire dont les colonnes clés sont ordonnées par cardinalité décroissante :
À présent, les lignes de la table sont d’abord ordonnées selon leur valeur ch, et les lignes qui ont la même valeur ch sont ordonnées selon leur valeur cl.
Mais comme la première colonne clé ch a une forte cardinalité, il est peu probable qu’il existe des lignes avec la même valeur ch. De ce fait, il est également peu probable que les valeurs cl soient ordonnées (localement — pour les lignes ayant la même valeur ch).
Par conséquent, les valeurs cl sont très probablement dans un ordre aléatoire et présentent donc, respectivement, une mauvaise localité et un mauvais taux de compression.
Résumé
Identifier efficacement des lignes individuelles
Un exemple concret
- l’ordre d’insertion des lignes lorsque le contenu change (par exemple à cause des frappes au clavier lors de la saisie du texte dans la zone de texte) et
- l’ordre sur disque des données issues des lignes insérées lorsque la
PRIMARY KEY (hash)est utilisée :
hash est utilisée comme colonne de clé primaire
- des lignes spécifiques peuvent être récupérées très rapidement, mais
- les lignes de la table (les données de leurs colonnes) sont stockées sur disque par ordre croissant des valeurs de hachage (uniques et aléatoires). Par conséquent, les valeurs de la colonne de contenu sont elles aussi stockées dans un ordre aléatoire, sans localité des données, ce qui entraîne un taux de compression sous-optimal pour le fichier de données de la colonne de contenu.
- un hachage du contenu, comme indiqué plus haut, qui est distinct pour des données distinctes, et
- un hachage sensible à la localité (empreinte) qui ne change pas lors de petites modifications des données.
- l’ordre d’insertion des lignes lorsque le contenu change (par exemple à cause des frappes au clavier lors de la saisie du texte dans la zone de texte) et
- l’ordre sur disque des données issues des lignes insérées lorsque la
PRIMARY KEY (fingerprint, hash)composite est utilisée :
fingerprint, et pour les lignes ayant la même valeur de fingerprint, c’est leur valeur hash qui détermine l’ordre final.
Comme les données qui ne diffèrent que par de petites modifications reçoivent la même valeur d’empreinte, des données similaires sont désormais stockées sur disque à proximité les unes des autres dans la colonne de contenu. C’est très bénéfique pour le taux de compression de la colonne de contenu, car, en règle générale, un algorithme de compression bénéficie de la localité des données (plus les données se ressemblent, meilleur est le taux de compression).
La contrepartie est que deux champs (fingerprint et hash) sont nécessaires pour récupérer une ligne spécifique afin d’utiliser de manière optimale l’index primaire résultant de la PRIMARY KEY (fingerprint, hash) composite.