Passer au contenu principal
Le type de données 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.
Exemples Créez une table avec une colonne de type map :
Query
CREATE TABLE tab (m Map(String, UInt64)) ENGINE=Memory;
INSERT INTO tab VALUES ({'key1':1, 'key2':10}), ({'key1':2,'key2':20}), ({'key1':3,'key2':30});
Pour sélectionner les valeurs de key2 :
Query
SELECT m['key2'] FROM tab;
Response
┌─arrayElement(m, 'key2')─┐
│                      10 │
│                      20 │
│                      30 │
└─────────────────────────┘
Si la clé demandée 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
CREATE TABLE tab (m Map(String, UInt64)) ENGINE=Memory;
INSERT INTO tab VALUES ({'key1':100}), ({});
SELECT m['key1'] FROM tab;
Response
┌─arrayElement(m, 'key1')─┐
│                     100 │
│                       0 │
└─────────────────────────┘

Conversion de Tuple en Map

Les valeurs de type Tuple() peuvent être converties en valeurs de type Map() à l’aide de la fonction CAST : Exemple
Query
SELECT CAST(([1, 2, 3], ['Ready', 'Steady', 'Go']), 'Map(UInt8, String)') AS map;
Response
┌─map───────────────────────────┐
│ {1:'Ready',2:'Steady',3:'Go'} │
└───────────────────────────────┘

Lire les sous-colonnes d’une Map

Pour éviter de lire toute la map, vous pouvez utiliser, dans certains cas, les sous-colonnes keys et values. Exemple
Query
CREATE TABLE tab (m Map(String, UInt64)) ENGINE = Memory;
INSERT INTO tab VALUES (map('key1', 1, 'key2', 2, 'key3', 3));

SELECT m.keys FROM tab; --   same as mapKeys(m)
SELECT m.values FROM tab; -- same as mapValues(m)
Response
┌─m.keys─────────────────┐
│ ['key1','key2','key3'] │
└────────────────────────┘

┌─m.values─┐
│ [1,2,3]  │
└──────────┘

Sérialisation des maps par buckets dans MergeTree

Par défaut, une colonne 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

CREATE TABLE tab (id UInt64, m Map(String, UInt64))
ENGINE = MergeTree ORDER BY id
SETTINGS
    map_serialization_version = 'with_buckets',
    max_buckets_in_map = 32,
    map_buckets_strategy = 'sqrt';
Pour éviter de ralentir les insertions, vous pouvez conserver la sérialisation basic pour les parts de niveau zéro (créées lors de INSERT) et n’utiliser with_buckets que pour les parts fusionnées :
CREATE TABLE tab (id UInt64, m Map(String, UInt64))
ENGINE = MergeTree ORDER BY id
SETTINGS
    map_serialization_version = 'with_buckets',
    map_serialization_version_for_zero_level_parts = 'basic',
    max_buckets_in_map = 32,
    map_buckets_strategy = 'sqrt';

Fonctionnement

Lorsqu’une part de données est écrite avec la sérialisation with_buckets :
  1. Le nombre moyen de clés par ligne est calculé à partir des statistiques du block.
  2. Le nombre de buckets est déterminé par la stratégie configurée (voir Paramètres).
  3. Chaque paire clé-valeur est affectée à un bucket en hachant la clé : bucket = hash(key) % num_buckets.
  4. Chaque bucket est stocké sous forme de sous-flux indépendant, avec ses propres clés, valeurs et offsets.
  5. Un flux de métadonnées buckets_info enregistre le nombre de buckets et les statistiques.
Lorsqu’une requête lit une clé spécifique (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é.
Le nombre de buckets peut varier d’une part à l’autre. Lorsque des parts avec des nombres de buckets différents sont fusionnées, le nombre de buckets de la nouvelle part est recalculé à partir des statistiques fusionnées. Des parts avec les sérialisations basic et with_buckets peuvent coexister dans la même table et être fusionnées de manière transparente.

Paramètres

ParamètreValeur par défautDescription
map_serialization_versionbasicFormat 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_partsbasicFormat 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_map32Limite 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_strategysqrtStraté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_coefficient1.0Multiplicateur pour les stratégies sqrt et linear. Ignoré lorsque la stratégie est constant.
map_buckets_min_avg_size32Nombre 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

Le tableau suivant résume l’impact sur les performances de 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ération10 clés100 clés1 000 clés10 000 clésRemarques
Recherche d’une seule clé (m['key'])1.6–3.2x plus rapide4.5–7.7x plus rapide16–39x plus rapide21–49x plus rapideLit un seul bucket au lieu de la colonne entière.
5 recherches de clés~1x1.5–3.1x plus rapide2.9–8.3x plus rapide4.5–6.7x plus rapideChaque clé lit son propre bucket ; certains buckets peuvent se chevaucher.
PREWHERE (SELECT m WHERE m['key'] = ...)1.5–3.0x plus rapide2.9–7.3x plus rapide5.3–31x plus rapide20–45x plus rapideLe 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 lentNécessite de lire et de réassembler tous les buckets.
INSERT1.5–2.5x plus lent1.5–2.5x plus lent1.5–2.5x plus lent1.5–2.5x plus lentSurcoû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éfaut map_buckets_min_avg_size = 32 l’impose automatiquement.
  • Maps moyennes (32–100 clés) : Utilisez with_buckets avec la stratégie sqrt si 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. Envisagez map_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_buckets avec les parts de niveau zéro définies sur basic. L’optimisation PREWHERE ne 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

Si la sérialisation 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

Le type de données JSON stocke chaque chemin fréquent dans une sous-colonne dynamique distincte. Les chemins qui dépassent la limite 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.
AspectMap with bucketsJSON
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 valeurToutes les valeurs partagent le même type VChaque chemin peut avoir son propre type. Les chemins sans indication de type utilisent Dynamic.
Prise en charge des skip indexesFonctionne avec certains types d’index créés sur mapKeys/mapValuesLes 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èteEnviron 2x plus lent que basic en raison du réassemblage des bucketsSurcoût dû à l’encodage du type Dynamic et à la reconstruction des chemins.
Surcoût de stockageMétadonnées supplémentaires minimalesPlus é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émaTypes de clé et de valeur fixés lors de la création de la tableEntiè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.
Utilisez 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

Vous pouvez diviser manuellement une seule Map en plusieurs colonnes selon le hachage des clés, au niveau de l’application :
CREATE TABLE tab (
    id UInt64,
    m0 Map(String, UInt64),
    m1 Map(String, UInt64),
    m2 Map(String, UInt64),
    m3 Map(String, UInt64)
) ENGINE = MergeTree ORDER BY id;
Lors de l’insertion, acheminez chaque paire clé-valeur vers la colonne m{hash(key) % 4}. Lors des requêtes, lisez la colonne correspondante : m{hash('target_key') % 4}['target_key'].
AspectMap avec bucketsSharding manuel
Facilité d’utilisationTransparent — géré par le moteur de stockageNécessite une logique de routage au niveau de l’application pour les inserts et les selects
Vertical MergeNon pris en charge — tous les buckets appartiennent à une seule colonnePris en charge — chaque colonne Map est indépendante et peut être fusionnée verticalement
Modifications du schémaLe nombre de buckets s’adapte automatiquement par partModifier le nombre de shards nécessite de réécrire les données ou d’ajouter de nouvelles colonnes
Syntaxe de requêtem['key'] fonctionne directementIl faut calculer la bonne colonne : m0['key'], m1['key'], etc.
Granularité des bucketsPar part, s’adapte aux statistiques des donnéesFixe à la création de la table
Le sharding manuel est utile lorsque les Vertical Merges sont importantes pour réduire l’utilisation mémoire lors des merges de tables comportant de nombreuses colonnes, ou lorsque le nombre de shards doit être fixe et explicitement contrôlé. Pour la plupart des cas d’usage, la sérialisation automatique par buckets est plus simple et suffisante. Voir aussi
Dernière modification le 29 juin 2026