Pular para o conteúdo principal
O tipo de dado Map(K, V) armazena pares chave-valor. Ao contrário de outros bancos de dados, os maps no ClickHouse não exigem chaves únicas, ou seja, um map pode conter dois elementos com a mesma chave. (O motivo é que os maps são implementados internamente como Array(Tuple(K, V)).) Você pode usar a sintaxe m[k] para obter o valor da chave k no map m. Além disso, m[k] faz uma varredura no map, ou seja, o runtime da operação é linear em relação ao tamanho do map. Parâmetros
  • K — O tipo das chaves do Map. Qualquer tipo, exceto Nullable e LowCardinality aninhado com tipos Nullable.
  • V — O tipo dos valores do Map. Qualquer tipo.
Exemplos Crie uma tabela com uma coluna do tipo 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});
Para selecionar os valores de key2:
Query
SELECT m['key2'] FROM tab;
Response
┌─arrayElement(m, 'key2')─┐
│                      10 │
│                      20 │
│                      30 │
└─────────────────────────┘
Se a chave solicitada k não estiver presente no map, m[k] retornará o default value do tipo de valor, por exemplo, 0 para tipos inteiros e '' para tipos String. Para verificar se uma chave existe em um map, você pode usar a função 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 │
└─────────────────────────┘

Convertendo Tuple para Map

Valores do tipo Tuple() podem ser convertidos para o tipo Map() usando a função CAST: Exemplo
Query
SELECT CAST(([1, 2, 3], ['Ready', 'Steady', 'Go']), 'Map(UInt8, String)') AS map;
Response
┌─map───────────────────────────┐
│ {1:'Ready',2:'Steady',3:'Go'} │
└───────────────────────────────┘

Leitura das subcolunas de Map

Para evitar ler o Map inteiro, em alguns casos você pode usar as subcolunas keys e values. Exemplo
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; --   mesmo que mapKeys(m)
SELECT m.values FROM tab; -- mesmo que mapValues(m)
Response
┌─m.keys─────────────────┐
│ ['key1','key2','key3'] │
└────────────────────────┘

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

Serialização em buckets de Map no MergeTree

Por padrão, uma coluna Map no MergeTree é armazenada como um único fluxo Array(Tuple(K, V)). Ler uma única chave com m['key'] exige varrer a coluna inteira — cada par chave-valor de cada linha — mesmo quando apenas uma chave é necessária. Em maps com muitas chaves distintas, isso se torna um gargalo. A serialização em buckets (with_buckets) divide os pares chave-valor em vários subfluxos independentes (buckets) com base no hash da chave. Quando uma consulta acessa m['key'], apenas o bucket que contém essa chave é lido do disco, ignorando todos os outros buckets.

Habilitando a serialização em buckets

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';
Para evitar deixar as inserções mais lentas, você pode manter a serialização basic para as partes de nível zero (criadas durante INSERT) e usar with_buckets apenas para as partes mescladas:
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';

Como funciona

Quando uma parte de dados é gravada com a serialização with_buckets:
  1. O número médio de chaves por linha é calculado com base nas estatísticas do bloco.
  2. O número de buckets é determinado pela estratégia configurada (consulte Configurações).
  3. Cada par chave-valor é atribuído a um bucket por meio do hash da chave: bucket = hash(key) % num_buckets.
  4. Cada bucket é armazenado como um subfluxo independente com suas próprias chaves, valores e offsets.
  5. Um fluxo de metadados buckets_info registra a contagem de buckets e as estatísticas.
Quando uma consulta lê uma chave específica (m['key']), o otimizador reescreve a expressão para uma subcoluna de chave (m.key_<serialized_key>). A camada de serialização calcula a qual bucket a chave solicitada pertence e lê apenas esse bucket do disco. Quando o map completo é lido (por exemplo, SELECT m), todos os buckets são lidos e remontados no map original. Isso é mais lento do que a serialização basic devido à sobrecarga de ler e mesclar vários subfluxos.
A ordem das chaves dentro de um valor de map pode diferir da ordem de inserção original ao usar a serialização with_buckets. As chaves são distribuídas entre buckets por hash e remontadas na ordem dos buckets, não na ordem de inserção. Com a serialização basic, a ordem das chaves dos maps inseridos é preservada.
A contagem de buckets pode variar entre partes. Quando partes com diferentes contagens de buckets são mescladas, a contagem de buckets da nova parte é recalculada com base nas estatísticas mescladas. Partes com serialização basic e with_buckets podem coexistir na mesma tabela e são mescladas de forma transparente.

Configurações

ConfiguraçãoPadrãoDescrição
map_serialization_versionbasicFormato de serialização para colunas Map. basic armazena os dados como um único fluxo de array. with_buckets divide as chaves em buckets para leituras mais rápidas de chaves individuais.
map_serialization_version_for_zero_level_partsbasicFormato de serialização para partes de nível zero (criadas por INSERT). Permite manter basic para operações de INSERT a fim de evitar sobrecarga de gravação, enquanto as partes mescladas usam with_buckets.
max_buckets_in_map32Limite superior para o número de buckets. A quantidade real depende de map_buckets_strategy. O valor máximo permitido é 256.
map_buckets_strategysqrtEstratégia para calcular a quantidade de buckets com base no tamanho médio do map: constant — sempre usa max_buckets_in_map; sqrt — usa round(coefficient * sqrt(avg_size)); linear — usa round(coefficient * avg_size). O resultado é limitado ao intervalo [1, max_buckets_in_map].
map_buckets_coefficient1.0Multiplicador para as estratégias sqrt e linear. Ignorado quando a estratégia é constant.
map_buckets_min_avg_size32Número médio mínimo de chaves por linha para habilitar o uso de buckets. Se a média ficar abaixo desse limite, um único bucket será usado independentemente das outras configurações. Defina como 0 para desabilitar esse limite.

Trade-offs de desempenho

A tabela a seguir resume o impacto no desempenho de with_buckets em comparação com a serialização basic em diferentes tamanhos de map (de 10 a 10.000 chaves por linha). A quantidade de buckets foi determinada pela estratégia sqrt, limitada a 32. Os números exatos dependem dos tipos de chave/valor, da distribuição dos dados e do hardware.
Operação10 chaves100 chaves1.000 chaves10.000 chavesObservações
Busca de uma única chave (m['key'])1.6–3.2x mais rápido4.5–7.7x mais rápido16–39x mais rápido21–49x mais rápidoLê apenas um bucket em vez da coluna inteira.
Busca de 5 chaves~1x1.5–3.1x mais rápido2.9–8.3x mais rápido4.5–6.7x mais rápidoCada chave lê seu próprio bucket; alguns buckets podem se sobrepor.
PREWHERE (SELECT m WHERE m['key'] = ...)1.5–3.0x mais rápido2.9–7.3x mais rápido5.3–31x mais rápido20–45x mais rápidoO filtro PREWHERE lê apenas um bucket; a leitura completa do map ocorre apenas para as linhas correspondentes. O ganho de desempenho depende da seletividade — menos grânulos correspondentes significam menos E/S do map completo.
Varredura completa do map (SELECT m)~2x mais lento~2x mais lento~2x mais lento~2x mais lentoÉ necessário ler e remontar todos os buckets.
INSERT1.5–2.5x mais lento1.5–2.5x mais lento1.5–2.5x mais lento1.5–2.5x mais lentoSobrecarga de aplicar hash às chaves e gravar em vários subfluxos.

Recomendações

  • Maps pequenas (< 32 chaves em média): Mantenha a serialização basic. A sobrecarga da divisão em buckets não se justifica para maps pequenas. O valor padrão map_buckets_min_avg_size = 32 garante isso automaticamente.
  • Maps médias (32–100 chaves): Use with_buckets com a estratégia sqrt se as consultas acessarem chaves individuais com frequência. O ganho de velocidade é de 4–8x para buscas de uma única chave.
  • Maps grandes (100+ chaves): Use with_buckets. Buscas de uma única chave são 16–49x mais rápidos. Considere map_serialization_version_for_zero_level_parts = 'basic' para manter a velocidade de inserção próxima à referência.
  • Varreduras completas do map dominam o workload: Mantenha basic. A serialização em buckets adiciona ~2x de sobrecarga para varreduras completas.
  • Workload misto (alguns buscas de chave, algumas varreduras completas): Use with_buckets com as partes de nível zero definidas como basic. A otimização PREWHERE lê apenas o bucket relevante para o filtro e depois lê o map completo somente para as linhas correspondentes, proporcionando um ganho líquido de velocidade significativo.

Abordagens alternativas

Se a serialização Map em buckets não se adequar ao seu caso de uso, há duas abordagens alternativas para melhorar o desempenho do acesso por chave:

Usando o tipo de dados JSON

O tipo de dados JSON armazena cada caminho frequente como uma subcoluna dinâmica separada. Os caminhos que excedem o limite de max_dynamic_paths vão para uma estrutura de dados compartilhada, que pode usar a serialização advanced para otimizar leituras de um único caminho. Consulte o artigo no blog para ter uma visão geral detalhada da serialização advanced.
AspectoMap com bucketsJSON
Leitura de uma única chaveLê um bucket (que pode conter outras chaves). Todos os pares chave-valor no bucket são desserializados.Caminhos frequentes são lidos diretamente das subcolunas dinâmicas. Caminhos pouco frequentes vão para dados compartilhados; com a serialização advanced, apenas os dados do caminho exato são lidos.
Tipos de valorTodos os valores compartilham o mesmo tipo VCada caminho pode ter seu próprio tipo. Caminhos sem dica de tipo usam Dynamic.
Suporte a índice de saltoFunciona com alguns tipos de índice criados em mapKeys/mapValuesÍndices de salto só podem ser criados em subcolunas de caminhos específicos, não em todos os caminhos/valores de uma só vez.
Leitura da coluna completa~2x mais lenta que basic devido à remontagem dos bucketsSobrecarga da codificação do tipo Dynamic e da reconstrução dos caminhos.
Sobrecarga de armazenamentoMetadados adicionais mínimosMaior devido à codificação do tipo Dynamic, ao armazenamento dos nomes dos caminhos e aos metadados adicionais na serialização advanced.
Flexibilidade de esquemaTipos fixos de chave e valor na criação da tabelaTotalmente dinâmica — chaves e tipos de valor podem variar por linha. Dicas de caminhos tipados podem ser declaradas para caminhos conhecidos, permitindo acesso direto por subcoluna.
Use JSON quando chaves diferentes precisarem de tipos de valor diferentes, quando o conjunto de chaves variar significativamente entre as linhas ou quando chaves acessadas com frequência forem conhecidas de antemão e puderem ser declaradas como caminhos tipados para acesso direto por subcoluna.

Sharding manual em várias colunas do tipo Map

Você pode dividir manualmente um único Map em várias colunas com base no hash da chave, no nível da aplicação:
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;
Durante a inserção, direcione cada par chave-valor para a coluna m{hash(key) % 4}. Nas consultas, leia da coluna específica: m{hash('target_key') % 4}['target_key'].
AspectoMap com bucketsSharding manual
Facilidade de usoTransparente — gerenciado pelo mecanismo de armazenamentoRequer lógica de roteamento no nível da aplicação para inserções e consultas
Mesclagem verticalNão suportada — todos os buckets pertencem a uma colunaSuportada — cada coluna Map é uma coluna independente e pode passar por mesclagem vertical
Alterações de schemaA quantidade de buckets se adapta automaticamente por parteAlterar o número de shards exige reescrever os dados ou adicionar novas colunas
Sintaxe de consultam['key'] funciona diretamenteÉ preciso calcular a coluna correta: m0['key'], m1['key'] etc.
Granularidade do bucketPor parte, adapta-se às estatísticas dos dadosFixa na criação da tabela
O sharding manual é vantajoso quando as mesclagens verticais são importantes para reduzir o uso de memória durante mesclagens de tabelas com muitas colunas, ou quando o número de shards precisa ser fixo e controlado explicitamente. Para a maioria dos casos de uso, a serialização automática em buckets é mais simples e suficiente. Veja também
Última modificação em 10 de junho de 2026