Skip to content
ClickHouse Docs
ClickHouse DocsClickHouse Docs

Map(K, V)

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:

Querysql
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:

Querysql
SELECT m['key2'] FROM tab;
Responsetext
┌─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.

Querysql
CREATE TABLE tab (m Map(String, UInt64)) ENGINE=Memory;
INSERT INTO tab VALUES ({'key1':100}), ({});
SELECT m['key1'] FROM tab;
Responsetext
┌─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

Querysql
SELECT CAST(([1, 2, 3], ['Ready', 'Steady', 'Go']), 'Map(UInt8, String)') AS map;
Responsetext
┌─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

Querysql
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)
Responsetext
┌─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 quantidade 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 quantidade de buckets pode variar entre partes. Quando partes com diferentes quantidades de buckets são mescladas, a quantidade 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ção Padrão Descrição
map_serialization_version basic Formato 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_parts basic Formato 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_map 32 Limite superior para o número de buckets. A quantidade real depende de map_buckets_strategy. O valor máximo permitido é 256.
map_buckets_strategy sqrt Estraté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_coefficient 1.0 Multiplicador para as estratégias sqrt e linear. Ignorado quando a estratégia é constant.
map_buckets_min_avg_size 32 Nú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ção 10 chaves 100 chaves 1.000 chaves 10.000 chaves Observações
Busca de uma única chave (m['key']) 1.6–3.2x mais rápido 4.5–7.7x mais rápido 16–39x mais rápido 21–49x mais rápido Lê apenas um bucket em vez da coluna inteira.
Busca de 5 chaves ~1x 1.5–3.1x mais rápido 2.9–8.3x mais rápido 4.5–6.7x mais rápido Cada chave lê seu próprio bucket; alguns buckets podem se sobrepor.
PREWHERE (SELECT m WHERE m['key'] = ...) 1.5–3.0x mais rápido 2.9–7.3x mais rápido 5.3–31x mais rápido 20–45x mais rápido O 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.
INSERT 1.5–2.5x mais lento 1.5–2.5x mais lento 1.5–2.5x mais lento 1.5–2.5x mais lento Sobrecarga 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.

Aspecto Map com buckets JSON
Leitura de uma única chave Lê 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 valor Todos os valores compartilham o mesmo tipo V Cada caminho pode ter seu próprio tipo. Caminhos sem dica de tipo usam Dynamic.
Suporte a índice de salto Funciona 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 buckets Sobrecarga da codificação do tipo Dynamic e da reconstrução dos caminhos.
Sobrecarga de armazenamento Metadados adicionais mínimos Maior devido à codificação do tipo Dynamic, ao armazenamento dos nomes dos caminhos e aos metadados adicionais na serialização advanced.
Flexibilidade de esquema Tipos fixos de chave e valor na criação da tabela Totalmente 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'].

Aspecto Map com buckets Sharding manual
Facilidade de uso Transparente — gerenciado pelo mecanismo de armazenamento Requer lógica de roteamento no nível da aplicação para inserções e consultas
Mesclagem vertical Não suportada — todos os buckets pertencem a uma coluna Suportada — cada coluna Map é uma coluna independente e pode passar por mesclagem vertical
Alterações de schema A quantidade de buckets se adapta automaticamente por parte Alterar o número de shards exige reescrever os dados ou adicionar novas colunas
Sintaxe de consulta m['key'] funciona diretamente É preciso calcular a coluna correta: m0['key'], m1['key'] etc.
Granularidade do bucket Por parte, adapta-se às estatísticas dos dados Fixa 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

Navigation