Map(K, V) stocke des paires clé-valeur.
Contrairement à d’autres bases de données, les maps ne sont pas uniques dans ClickHouse, c.-à-d. qu’une map peut contenir deux éléments ayant la même clé.
(Cela s’explique par le fait que les maps sont implémentées en interne sous la forme de Array(Tuple(K, V)).)
Vous pouvez utiliser la syntaxe m[k] pour obtenir la valeur associée à la clé k dans la map m.
De plus, m[k] parcourt la map, c.-à-d. que le temps d’exécution de l’opération est linéaire en fonction de la taille de la map.
Paramètres
K— Le type des clés de la Map. N’importe quel type, à l’exception de Nullable et de LowCardinality imbriqué avec des types Nullable.V— Le type des valeurs de la Map. N’importe quel type.
Query
key2 :
Query
Response
k n’est pas présente dans la map, m[k] renvoie la valeur par défaut du type de valeur, par exemple 0 pour les types entiers et '' pour les types String.
Pour vérifier si une clé existe dans une map, vous pouvez utiliser la fonction mapContains.
Query
Response
Conversion de Tuple en Map
Tuple() peuvent être converties en valeurs de type Map() à l’aide de la fonction CAST :
Exemple
Query
Response
Lire les sous-colonnes d’une Map
keys et values.
Exemple
Query
Response
Sérialisation des maps par buckets dans MergeTree
Map dans MergeTree est stockée dans un unique flux Array(Tuple(K, V)).
Lire une seule clé avec m['key'] nécessite de parcourir toute la colonne — chaque paire clé-valeur de chaque ligne — même si une seule clé est nécessaire.
Pour les maps contenant de nombreuses clés distinctes, cela devient un goulot d’étranglement.
La sérialisation par buckets (with_buckets) répartit les paires clé-valeur dans plusieurs sous-flux indépendants (buckets) en appliquant un hash à la clé.
Lorsqu’une requête accède à m['key'], seul le bucket qui contient cette clé est lu sur le disque, les autres buckets étant ignorés.
Activation de la sérialisation par bucket
basic pour les parts de niveau zéro (créées lors de INSERT) et n’utiliser with_buckets que pour les parts fusionnées :
Fonctionnement
with_buckets :
- Le nombre moyen de clés par ligne est calculé à partir des statistiques du block.
- Le nombre de buckets est déterminé par la stratégie configurée (voir Paramètres).
- Chaque paire clé-valeur est affectée à un bucket en hachant la clé :
bucket = hash(key) % num_buckets. - Chaque bucket est stocké sous forme de sous-flux indépendant, avec ses propres clés, valeurs et offsets.
- Un flux de métadonnées
buckets_infoenregistre le nombre de buckets et les statistiques.
m['key']), l’optimiseur réécrit l’expression en sous-colonne de clé (m.key_<serialized_key>).
La couche de sérialisation détermine à quel bucket appartient la clé demandée et ne lit que ce bucket sur le disque.
Lorsque la map complète est lue (par ex. SELECT m), tous les buckets sont lus puis réassemblés dans la map d’origine. Cette opération est plus lente qu’avec la sérialisation basic, en raison de la surcharge liée à la lecture et à la fusion de plusieurs sous-flux.
L’ordre des clés dans une valeur de map peut différer de l’ordre d’insertion initial lors de l’utilisation de la sérialisation
with_buckets. Les clés sont réparties entre les buckets par hachage, puis réassemblées dans l’ordre des buckets, et non dans l’ordre d’insertion. Avec la sérialisation basic, l’ordre des clés des maps insérées est conservé.basic et with_buckets peuvent coexister dans la même table et être fusionnées de manière transparente.
Paramètres
| Paramètre | Valeur par défaut | Description |
|---|---|---|
map_serialization_version | basic | Format de sérialisation des colonnes Map. basic stocke les données dans un seul flux de type tableau. with_buckets répartit les clés dans des buckets pour accélérer la lecture d’une clé unique. |
map_serialization_version_for_zero_level_parts | basic | Format de sérialisation pour les parts de niveau zéro (créées par INSERT). Permet de conserver basic pour les insertions afin d’éviter la surcharge d’écriture, tandis que les parts fusionnées utilisent with_buckets. |
max_buckets_in_map | 32 | Limite supérieure du nombre de buckets. Le nombre réel dépend de map_buckets_strategy. La valeur maximale autorisée est 256. |
map_buckets_strategy | sqrt | Stratégie de calcul du nombre de buckets à partir de la taille moyenne de la map : constant — utilise toujours max_buckets_in_map ; sqrt — utilise round(coefficient * sqrt(avg_size)) ; linear — utilise round(coefficient * avg_size). Le résultat est limité à [1, max_buckets_in_map]. |
map_buckets_coefficient | 1.0 | Multiplicateur pour les stratégies sqrt et linear. Ignoré lorsque la stratégie est constant. |
map_buckets_min_avg_size | 32 | Nombre moyen minimal de clés par ligne pour activer le partitionnement en buckets. Si la moyenne est inférieure à ce seuil, un seul bucket est utilisé quels que soient les autres paramètres. Définissez cette valeur sur 0 pour désactiver le seuil. |
Compromis en matière de performances
with_buckets par rapport à la sérialisation basic pour différentes tailles de Map (de 10 à 10 000 clés par ligne). Le nombre de buckets a été déterminé à l’aide de la stratégie sqrt, plafonnée à 32. Les chiffres exacts dépendent des types de clés/valeurs, de la distribution des données et du matériel.
| Opération | 10 clés | 100 clés | 1 000 clés | 10 000 clés | Remarques |
|---|---|---|---|---|---|
Recherche d’une seule clé (m['key']) | 1.6–3.2x plus rapide | 4.5–7.7x plus rapide | 16–39x plus rapide | 21–49x plus rapide | Lit un seul bucket au lieu de la colonne entière. |
| 5 recherches de clés | ~1x | 1.5–3.1x plus rapide | 2.9–8.3x plus rapide | 4.5–6.7x plus rapide | Chaque clé lit son propre bucket ; certains buckets peuvent se chevaucher. |
PREWHERE (SELECT m WHERE m['key'] = ...) | 1.5–3.0x plus rapide | 2.9–7.3x plus rapide | 5.3–31x plus rapide | 20–45x plus rapide | Le filtre PREWHERE ne lit qu’un seul bucket ; la Map complète n’est lue que pour les lignes correspondantes. Le gain de performance dépend de la sélectivité — moins il y a de granules correspondants, moins il y a d’E/S sur la Map complète. |
Parcours complet de la Map (SELECT m) | ~2x plus lent | ~2x plus lent | ~2x plus lent | ~2x plus lent | Nécessite de lire et de réassembler tous les buckets. |
| INSERT | 1.5–2.5x plus lent | 1.5–2.5x plus lent | 1.5–2.5x plus lent | 1.5–2.5x plus lent | Surcoût lié au hachage des clés et à l’écriture dans plusieurs sous-flux. |
Recommandations
- Petites maps (< 32 clés en moyenne) : Conservez la sérialisation
basic. Le surcoût de la répartition en buckets ne se justifie pas pour les petites maps. La valeur par défautmap_buckets_min_avg_size = 32l’impose automatiquement. - Maps moyennes (32–100 clés) : Utilisez
with_bucketsavec la stratégiesqrtsi les requêtes accèdent fréquemment à des clés individuelles. Le gain de performance est de 4 à 8x pour les recherches sur une seule clé. - Grandes maps (100+ clés) : Utilisez
with_buckets. Les recherches sur une seule clé sont 16 à 49x plus rapides. Envisagezmap_serialization_version_for_zero_level_parts = 'basic'pour conserver une vitesse d’insert proche de la référence. - Les scans complets de maps dominent la charge de travail : Conservez
basic. La sérialisation par buckets ajoute un surcoût d’environ 2x pour les scans complets. - Charge de travail mixte (certaines recherches de clé, certains scans complets) : Utilisez
with_bucketsavec les parts de niveau zéro définies surbasic. L’optimisationPREWHEREne lit que le bucket pertinent pour le filtre, puis ne lit la map complète que pour les lignes correspondantes, ce qui se traduit au final par un gain de performance net significatif.
Approches alternatives
Map par buckets ne correspond pas à votre cas d’utilisation, il existe deux autres approches pour améliorer les performances d’accès au niveau des clés :
Utilisation du type de données JSON
max_dynamic_paths sont placés dans une structure de données partagée, qui peut utiliser la sérialisation advanced pour optimiser la lecture d’un chemin unique. Consultez le billet de blog pour une présentation détaillée de la sérialisation advanced.
| Aspect | Map with buckets | JSON |
|---|---|---|
| Lecture d’une seule clé | Lit un bucket (qui peut contenir d’autres clés). Toutes les paires clé-valeur du bucket sont désérialisées. | Les chemins fréquents sont lus directement depuis les sous-colonnes dynamiques. Les chemins peu fréquents vont dans les données partagées ; avec la sérialisation advanced, seules les données du chemin exact sont lues. |
| Types de valeur | Toutes les valeurs partagent le même type V | Chaque chemin peut avoir son propre type. Les chemins sans indication de type utilisent Dynamic. |
| Prise en charge des skip indexes | Fonctionne avec certains types d’index créés sur mapKeys/mapValues | Les skip indexes ne peuvent être créés que sur des sous-colonnes de chemin spécifiques, et non sur tous les chemins/valeurs à la fois. |
| Lecture de la colonne complète | Environ 2x plus lent que basic en raison du réassemblage des buckets | Surcoût dû à l’encodage du type Dynamic et à la reconstruction des chemins. |
| Surcoût de stockage | Métadonnées supplémentaires minimales | Plus élevé en raison de l’encodage du type Dynamic, du stockage des noms de chemins et des métadonnées supplémentaires liées à la sérialisation advanced. |
| Flexibilité du schéma | Types de clé et de valeur fixés lors de la création de la table | Entièrement dynamique — les clés et les types de valeur peuvent varier d’une ligne à l’autre. Des indications de type pour les chemins connus peuvent être déclarées pour un accès direct aux sous-colonnes. |
JSON lorsque différentes clés nécessitent différents types de valeur, lorsque l’ensemble des clés varie fortement d’une ligne à l’autre, ou lorsque les clés fréquemment consultées sont connues à l’avance et peuvent être déclarées comme chemins typés pour un accès direct aux sous-colonnes.
Partitionnement manuel en plusieurs colonnes Map
Map en plusieurs colonnes selon le hachage des clés, au niveau de l’application :
m{hash(key) % 4}. Lors des requêtes, lisez la colonne correspondante : m{hash('target_key') % 4}['target_key'].
| Aspect | Map avec buckets | Sharding manuel |
|---|---|---|
| Facilité d’utilisation | Transparent — géré par le moteur de stockage | Nécessite une logique de routage au niveau de l’application pour les inserts et les selects |
| Vertical Merge | Non pris en charge — tous les buckets appartiennent à une seule colonne | Pris en charge — chaque colonne Map est indépendante et peut être fusionnée verticalement |
| Modifications du schéma | Le nombre de buckets s’adapte automatiquement par part | Modifier le nombre de shards nécessite de réécrire les données ou d’ajouter de nouvelles colonnes |
| Syntaxe de requête | m['key'] fonctionne directement | Il faut calculer la bonne colonne : m0['key'], m1['key'], etc. |
| Granularité des buckets | Par part, s’adapte aux statistiques des données | Fixe à la création de la table |
- fonction map()
- fonction CAST()
- combinateur -Map pour le type de données Map