Procurando um guia?
Confira nosso guia de boas práticas para JSON com exemplos, recursos avançados e considerações sobre o uso do tipo JSON.
JSON armazena documentos JavaScript Object Notation (JSON) em uma única coluna.
No ClickHouse Open-Source, o tipo de dados JSON é considerado pronto para produção a partir da versão 25.3. Não é recomendável usar esse tipo em produção em versões anteriores.
JSON, você pode usar a seguinte sintaxe:
| Parâmetro | Descrição | Valor padrão |
|---|---|---|
max_dynamic_paths | Um parâmetro opcional que indica quantos caminhos podem ser armazenados separadamente como subcolunas em um único bloco de dados armazenado separadamente (por exemplo, em uma única data part de uma tabela MergeTree). Se esse limite for excedido, todos os outros caminhos serão armazenados juntos em uma única estrutura chamada dados compartilhados. Também há formas de alterar o limite de caminhos dinâmicos sem mudar esse parâmetro. | 1024 |
max_dynamic_types | Um parâmetro opcional entre 1 e 255 que indica quantos tipos de dados diferentes podem ser armazenados separadamente em uma única coluna de caminho do tipo Dynamic em um único bloco de dados armazenado separadamente (por exemplo, em uma única data part de uma tabela MergeTree). Se esse limite for excedido, todos os novos tipos serão armazenados juntos em uma única estrutura chamada shared variant. | 32 |
some.path TypeName | Um type hint opcional para um caminho específico no JSON. Esses caminhos sempre serão armazenados como subcolunas com o tipo especificado. | |
SKIP path.to.skip | Um hint opcional para um caminho específico que deve ser ignorado durante o parsing do JSON. Esses caminhos nunca serão armazenados na coluna JSON. Se o caminho especificado for um objeto JSON aninhado, todo o objeto aninhado será ignorado. | |
SKIP REGEXP 'path_regexp' | Um hint opcional com uma expressão regular usada para ignorar caminhos durante o parsing do JSON. Todos os caminhos que corresponderem a essa expressão regular nunca serão armazenados na coluna JSON. |
Quando usar o tipo JSON
JSON foi projetado para consultar, filtrar e agregar campos específicos em objetos JSON com estruturas dinâmicas ou imprevisíveis. Ele faz isso dividindo objetos JSON em subcolunas separadas, o que reduz drasticamente a quantidade de dados lidos e acelera as consultas em campos selecionados em comparação com alternativas como Map ou o parsing de strings.
No entanto, isso envolve trade-offs importantes:
INSERTs mais lentos - Dividir JSON em subcolunas, realizar inferência de tipos e gerenciar estruturas de armazenamento flexíveis torna os inserts mais lentos em comparação com armazenar JSON como uma simples colunaString.- Mais lento ao ler objetos inteiros - Se você precisa recuperar documentos JSON completos (em vez de campos específicos), o tipo
JSONé mais lento do que ler de uma colunaString. A sobrecarga de reconstruir objetos a partir de subcolunas separadas não traz nenhum benefício quando você não está fazendo consultas no nível de campo. - Sobrecarga de armazenamento - Manter subcolunas separadas acrescenta uma sobrecarga estrutural em comparação com armazenar JSON como um único valor em string.
Use o tipo JSON quando:
- Seus dados têm uma estrutura dinâmica ou imprevisível, com chaves que variam entre documentos
- Os tipos de campo ou os esquemas mudam ao longo do tempo ou variam entre registros
- Você precisa consultar, filtrar ou agregar dados em caminhos específicos dentro de objetos JSON cuja estrutura não pode ser prevista antecipadamente
- Seu caso de uso envolve dados semiestruturados, como logs, eventos ou conteúdo gerado por usuários, com esquemas inconsistentes
Use uma coluna String (ou tipos estruturados) quando:
- A estrutura dos seus dados é conhecida e consistente — nesse caso, use colunas normais ou os tipos
Tuple,Array,DynamicouVariant - Documentos
JSONsão tratados como blobs opacos, apenas armazenados e recuperados integralmente, sem análise em nível de campo - Você não precisa consultar nem filtrar campos individuais do JSON dentro do banco de dados
- O
JSONé simplesmente um formato de transporte/armazenamento e não é analisado no ClickHouse
Criando JSON
JSON.
Usando JSON na definição de uma coluna de tabela
Query (Example 1)
Response (Example 1)
Query (Example 2)
Response (Example 2)
Usando CAST com ::JSON
::JSON.
CAST de String para JSON
Query
Response
CAST de Tuple para JSON
Query
Response
CAST de Map para JSON
Query
Response
Os caminhos JSON são armazenados de forma achatada. Isso significa que, quando um objeto JSON é reconstituído a partir de um caminho como retornará:e não:
a.b.c,
não é possível saber se o objeto deve ser construído como { "a.b.c" : ... } ou { "a": { "b": { "c": ... } } }.
Nossa implementação sempre assumirá a segunda forma.Por exemplo:Consulta
Resposta
Leitura de caminhos JSON como subcolunas
JSON oferece suporte à leitura de cada caminho como uma subcoluna separada.
Se o tipo do caminho solicitado não for especificado na declaração do tipo JSON,
a subcoluna desse caminho sempre terá o tipo Dynamic.
Por exemplo:
Query
Response
Query (Reading JSON paths as sub-columns)
Response (Reading JSON paths as sub-columns)
getSubcolumn para ler subcolunas do tipo JSON:
Query
Response
NULL:
Query
Response
Query
Response
a.b, o tipo é UInt32, conforme especificamos na declaração do tipo JSON,
e, para todas as outras subcolunas, o tipo é Dynamic.
Também é possível ler subcolunas de um tipo Dynamic usando a sintaxe especial json.some.path.:TypeName:
Query
Response
Dynamic podem ser convertidas para qualquer tipo de dado. Nesse caso, será lançada uma exceção se o tipo interno em Dynamic não puder ser convertido para o tipo solicitado:
Query
Response
Query
Response
Para ler subcolunas com eficiência em partes Compact do MergeTree, certifique-se de que a configuração do MergeTree write_marks_for_substreams_in_compact_parts esteja habilitada.
Leitura de sub-objetos JSON como subcolunas
JSON permite ler objetos aninhados como subcolunas do tipo JSON usando a sintaxe especial json.^some.path:
Query
Response
Query
Response
Quando os caminhos são armazenados em dados compartilhados básicos (
map), a leitura de subcolunas de subobjeto pode ser ineficiente, pois exige a varredura de toda a estrutura de dados compartilhados. Com a serialização de dados compartilhados map_with_buckets ou advanced, a leitura de subcolunas desses dados compartilhados é altamente otimizada.Leitura de subcolunas combinadas de JSON
JSON permite ler um caminho como uma subcoluna combinada usando a sintaxe especial json.@some.path.
Uma subcoluna combinada para um determinado caminho retorna:
- O valor literal armazenado nesse caminho como
Dynamic, se o caminho tiver um valor literal. - Um subobjeto JSON nesse caminho como
Dynamic, se o caminho não tiver um valor literal, mas tiver subcaminhos aninhados. NULL, se não existir nem um valor literal nem qualquer subcaminho para esse caminho.
json.a) e a subcoluna de subobjeto (json.^a).
O exemplo a seguir compara os três tipos de subcoluna para o caminho a:
Query
Response
Query
Response
- Linha 1:
acontém o literal42.json.ao retorna comoDynamic(Int64),json.^aretorna um subobjeto vazio{}(sem chaves aninhadas ema) ejson.@aretorna o literal42. - Linha 2:
acontém um objeto aninhado.json.aretornaNULL(não há literal nesse caminho),json.^aretorna o subobjeto comoJSONejson.@atambém retorna o subobjeto comoDynamic(JSON). - Linha 3:
aestá completamente ausente. Tantojson.aquantojson.@aretornamNULL, enquantojson.^aretorna um{}vazio.
Quando os caminhos são armazenados em dados compartilhados básicos (
map), a leitura de subcolunas combinadas pode ser ineficiente, pois exige varrer toda a estrutura de dados compartilhados. Com a serialização de dados compartilhados map_with_buckets ou advanced, a leitura de subcolunas a partir de dados compartilhados é altamente otimizada.Inferência de tipo para caminhos
JSON, o ClickHouse tenta detectar o tipo de dado mais adequado para cada caminho JSON.
Isso funciona de forma semelhante à inferência automática de esquema a partir dos dados de entrada,
e é controlado pelas mesmas configurações:
- input_format_try_infer_dates
- input_format_try_infer_datetimes
- schema_inference_make_columns_nullable
- input_format_json_try_infer_numbers_from_strings
- input_format_json_infer_incomplete_types_as_strings
- input_format_json_read_numbers_as_strings
- input_format_json_read_bools_as_strings
- input_format_json_read_bools_as_numbers
- input_format_json_read_arrays_as_strings
- input_format_json_infer_array_of_dynamic_from_array_of_different_types
Query
Response
Query
Response
Query
Response
Query
Response
Como lidar com arrays de objetos JSON
Array(JSON) e inseridos em uma coluna Dynamic para esse caminho.
Para ler um array de objetos, você pode extraí-lo da coluna Dynamic como uma subcoluna:
Query
Response
Query
Response
max_dynamic_types/max_dynamic_paths do tipo JSON aninhado foram reduzidos em relação aos valores padrão.
Isso é necessário para evitar que o número de subcolunas cresça de forma descontrolada em arrays aninhados de objetos JSON.
Vamos tentar ler subcolunas de uma coluna JSON aninhada:
Query
Response
Array(JSON) usando uma sintaxe especial:
Query
Response
[] após o caminho indica o nível do array. Por exemplo, json.path[][] será transformado em json.path.:Array(Array(JSON))
Vamos verificar os caminhos e tipos dentro do nosso Array(JSON):
Query
Response
Array(JSON):
Query
Response
JSON aninhada:
Query
Response
Tratamento de chaves JSON com NULL
null e a ausência de valor são considerados equivalentes:
Query
Response
Tratamento de chaves JSON com pontos
a.b e valor 42. Durante a formatação de JSON, sempre formamos objetos aninhados com base nas partes do caminho separadas por ponto:
Query
Response
{"a.b" : 42} agora está formatado como {"a" : {"b" : 42}}.
Essa limitação também faz com que o parsing de objetos JSON válidos como este falhe:
Query
Response
25.8). Nesse caso, durante o parsing, todos os pontos nas chaves JSON serão
escapados como %2E e restaurados durante a formatação.
Query
Response
Query
Response
Query
Response
parser de identificadores e do analyzer, a subcoluna json.`a.b` é equivalente à subcoluna json.a.b e não lerá o caminho com ponto escapado:
Query
Response
SKIP/SKIP REGEX), será necessário usar pontos com escape na dica:
Query
Response
Query
Response
Lendo o tipo JSON a partir de dados
JSONEachRow,
TSV,
CSV,
CustomSeparated,
Values, etc.) suportam a leitura do tipo JSON.
Exemplos:
Query
Response
CSV/TSV/etc., o JSON é obtido por parsing a partir de uma string que contém o objeto JSON:
Query
Response
Atingindo o limite de caminhos dinâmicos no JSON
JSON pode armazenar internamente apenas um número limitado de caminhos como subcolunas separadas.
Por padrão, esse limite é 1024, mas você pode alterá-lo na declaração do tipo usando o parâmetro max_dynamic_paths.
Quando o limite é atingido, todos os novos caminhos inseridos em uma coluna JSON serão armazenados em uma única estrutura de dados compartilhada.
Ainda é possível ler esses caminhos como subcolunas,
mas isso pode ser menos eficiente (veja a seção sobre dados compartilhados).
Esse limite é necessário para evitar um número enorme de subcolunas diferentes, o que pode tornar a tabela inutilizável.
Vamos ver o que acontece quando o limite é atingido em alguns cenários diferentes.
Ao atingir o limite durante o parsing dos dados
JSON dos dados, quando o limite é atingido para o bloco de dados atual,
todos os novos caminhos serão armazenados em uma estrutura de dados compartilhada. Podemos usar as duas funções de introspecção a seguir: JSONDynamicPaths, JSONSharedDataPaths
Query
Response
e e f.g, o limite foi atingido,
e eles foram inseridos em uma estrutura de dados compartilhada.
Durante mesclagens de partes de dados em motores de tabela MergeTree
MergeTree, a coluna JSON na parte de dados resultante pode atingir o limite de caminhos dinâmicos
e não conseguir armazenar todos os caminhos das partes de origem como subcolunas.
Nesse caso, o ClickHouse escolhe quais caminhos permanecerão como subcolunas após a mesclagem e quais caminhos serão armazenados na estrutura de dados compartilhada.
Na maioria dos casos, o ClickHouse tenta manter os caminhos que contêm
o maior número de valores não nulos e mover os caminhos mais raros para a estrutura de dados compartilhada. No entanto, isso depende da implementação.
Vamos ver um exemplo desse tipo de mesclagem.
Primeiro, vamos criar uma tabela com uma coluna JSON, definir o limite de caminhos dinâmicos como 3 e depois inserir valores com 5 caminhos diferentes:
Query
JSON contendo um único caminho:
Query
Response
Query
Response
a, b e c, que são os mais frequentes, e transferiu os caminhos d e e para uma estrutura de dados compartilhada.
Conforme descrito na seção anterior, quando o limite max_dynamic_paths é atingido, todos os novos caminhos são armazenados em uma única estrutura de dados compartilhada.
Nesta seção, veremos os detalhes da estrutura de dados compartilhada e como lemos as subcolunas de caminhos a partir dela.
Consulte a seção “funções de introspecção” para obter detalhes sobre as funções usadas para inspecionar o conteúdo de uma coluna JSON.
Em memória, a estrutura de dados compartilhada é apenas uma subcoluna do tipo Map(String, String) que armazena o mapeamento entre um caminho JSON achatado e um valor codificado em binário.
Para extrair dela a subcoluna de um caminho, basta iterar por todas as linhas nessa coluna Map e tentar encontrar o caminho solicitado e seus valores.
Em tabelas MergeTree, armazenamos os dados em partes de dados que mantêm tudo em disco (local ou remoto). E os dados em disco podem ser armazenados de uma forma diferente da memória.
Atualmente, existem 3 serializações diferentes da estrutura de dados compartilhada em partes de dados do MergeTree: map, map_with_buckets
e advanced.
A versão da serialização é controlada pelas
configurações do MergeTree object_shared_data_serialization_version
e object_shared_data_serialization_version_for_zero_level_parts
(a parte de nível zero é a parte criada durante a inserção de dados na tabela; durante operações de merge, as partes têm nível mais alto).
Observação: alterar a serialização da estrutura de dados compartilhada é compatível apenas
com a v3 versão de serialização de objeto
Na versão de serialização map, os dados compartilhados são serializados como uma única coluna do tipo Map(String, String), da mesma forma que são armazenados na
memória. Para ler uma subcoluna de caminho desse tipo de serialização, o ClickHouse lê toda a coluna Map e
extrai o caminho solicitado na memória.
Essa serialização é eficiente para gravar dados e ler toda a coluna JSON, mas não é eficiente para ler subcolunas de caminhos.
Na versão de serialização map_with_buckets, os dados compartilhados são serializados como N colunas (“buckets”) do tipo Map(String, String).
Cada bucket desse tipo contém apenas um subconjunto de caminhos. Para ler a subcoluna de um caminho nesse tipo de serialização, o ClickHouse
lê toda a coluna Map de um único bucket e extrai o caminho solicitado na memória.
Essa serialização é menos eficiente para gravar dados e ler a coluna JSON inteira, mas é mais eficiente para ler subcolunas de caminhos
porque lê dados apenas dos buckets necessários.
O número de buckets N é controlado pelas configurações do MergeTree object_shared_data_buckets_for_compact_part (8 por padrão)
e object_shared_data_buckets_for_wide_part (32 por padrão).
O valor máximo permitido para ambas as configurações é 256.
Na versão de serialização advanced, os dados compartilhados são serializados em uma estrutura de dados especial que maximiza o desempenho
da leitura de subcolunas de caminhos, armazenando algumas informações adicionais que permitem ler apenas os dados dos caminhos solicitados.
Essa serialização também oferece suporte a buckets, de modo que cada bucket contenha apenas um subconjunto de caminhos.
Essa serialização é bastante ineficiente para gravação de dados (portanto, não é recomendável usar essa serialização para partes de nível zero); a leitura da coluna JSON inteira é ligeiramente menos eficiente em comparação com a serialização map, mas ela é muito eficiente para ler subcolunas de caminhos.
Observação: devido ao armazenamento de algumas informações adicionais dentro da estrutura de dados, o uso de espaço em disco é maior com essa serialização em comparação com as
serializações map e map_with_buckets.
Para uma visão geral mais detalhada das novas serializações de dados compartilhados e dos detalhes de implementação, leia a postagem no blog.
Controlando o número de caminhos dinâmicos dentro de JSON em partes do MergeTree
max_dynamic_paths na declaração do tipo JSON.
Mas alterar max_dynamic_paths em colunas existentes exige executar ALTER TABLE <table> MODIFY COLUMN <column> JSON(max_dynamic_paths=K), o que iniciará uma mutação em segundo plano que reescreverá todas as partes existentes.
Essa mutação pode ser bastante pesada e pode afetar o desempenho do servidor até sua conclusão. Para evitar isso, você pode usar estas 3 configurações, que ajudam a alterar o limite de caminhos dinâmicos em tabelas MergeTree para novas partes de dados:
merge_max_dynamic_subcolumns_in_wide_part- uma configuração do MergeTree que limita o número de subcolunas dinâmicas de cada coluna JSON durante a mesclagem em uma parte de dados Wide.merge_max_dynamic_subcolumns_in_compact_part- uma configuração do MergeTree que limita o número de subcolunas dinâmicas de cada coluna JSON durante a mesclagem em uma parte de dados Compact.max_dynamic_subcolumns_in_json_type_parsing- uma configuração de sessão que limita o número de subcolunas dinâmicas de cada coluna JSON durante o parsing de dados JSON em uma coluna JSON.
max_dynamic_paths, mesmo que os valores das configurações descritas sejam maiores.
Funções de introspecção
JSONAllPathsJSONAllPathsWithTypesJSONAllValuesJSONDynamicPathsJSONDynamicPathsWithTypesJSONSharedDataPathsJSONSharedDataPathsWithTypesdistinctDynamicTypesdistinctJSONPaths and distinctJSONPathsAndTypes
2020-01-01:
Query
Response
Query
Response
ALTER MODIFY COLUMN para o tipo JSON
JSON. Atualmente, só há suporte a ALTER de um tipo String.
Exemplo
Query
Response
Lazy Type Hints (Experimental)
Este recurso é experimental e exige que a configuração
allow_experimental_json_lazy_type_hints esteja habilitada.ALTER TABLE ... MODIFY COLUMN, o ClickHouse normalmente reescreve todas as partes de dados para materializar os novos type hints. Em tabelas com grandes volumes de dados históricos (centenas de terabytes), isso pode ser extremamente custoso.
Lazy type hints permitem adicionar type hints como uma operação somente de metadados, sem reescrever os dados existentes:
- Partes antigas: os type hints são aplicados em tempo de consulta, convertendo de
Dynamicpara o tipo indicado - Partes novas: os type hints são materializados durante operações de
INSERT - Merges: os type hints são materializados quando as partes são mescladas
Como habilitar Lazy Type Hints
Exemplo
Query
Response
Verificando se não ocorreu nenhuma mutação
ALTER foi concluído sem gerar uma mutação consultando a tabela system.mutations:
Materializando type hints
- Aguardar as mesclagens em segundo plano: o ClickHouse materializa automaticamente os type hints quando as partes são mescladas
- Forçar a mesclagem: use
OPTIMIZE TABLE test_lazy FINALpara mesclar todas as partes imediatamente - Reescrever as partes: use
ALTER TABLE test_lazy REWRITE PARTSpara reescrever as partes com os novos metadados
Limitações
- Este recurso é experimental e pode mudar em versões futuras
- A conversão de tipos no momento da consulta pode ter um impacto significativo no desempenho em comparação com tipos pré-materializados, especialmente para objetos JSON grandes
- O recurso se aplica apenas à modificação de
typed_paths(type hints); outros parâmetros de JSON, comomax_dynamic_paths,SKIPouSKIP REGEXP, ainda exigem mutações
Comparação entre valores do tipo JSON
Query
Response
Variant.
Data skipping indexes para JSON
JSON de três formas:
- Índices em subcolunas específicas — crie um skip index padrão em um caminho JSON conhecido, assim como em uma coluna comum. Isso indexa os valores desse caminho.
- Índices baseados em caminhos com
JSONAllPaths— indexe o conjunto de caminhos presentes em cada grânulo para ignorar grânulos que não possam conter o caminho consultado. - Índices baseados em valores com
JSONAllValues— indexe todos os valores em todos os caminhos JSON usando um índice de texto para acelerar a busca textual completa em qualquer subcoluna JSON com um único índice.
Índices em subcolunas específicas
minmax, set, bloom_filter, tokenbf_v1, ngrambf_v1, etc.).
Há duas maneiras de referenciar uma subcoluna JSON em uma expressão de índice:
- Caminho tipado declarado no type hint JSON — acesse-o diretamente pelo nome:
json.a. - Caminho dinâmico com cast explícito — use a sintaxe de cast
:::json.b::String.
json.a || json.b::String.
Exemplo
Query
minmax na subcoluna tipada data.sensor_id restringe a varredura aos grânulos correspondentes:
Query
Response
bloom_filter na subcoluna com cast data.location::String também funciona:
Query
Response
Índices baseados em caminhos com JSONAllPaths
JSON usando a função JSONAllPaths.
Isso funciona de maneira semelhante à criação de skip indexes em colunas Map com mapKeys — o índice armazena o conjunto de caminhos JSON presentes em cada grânulo e o utiliza para ignorar os grânulos que não podem conter o caminho consultado.
Tipos de índice compatíveis
JSONAllPaths pode ser usado com os seguintes tipos de skip index:
bloom_filter— oferece suporte aequals,ineIS NOT NULL.tokenbf_v1— oferece suporte aequalseIS NOT NULL.ngrambf_v1— oferece suporte aequalseIS NOT NULL.text(índice invertido) — oferece suporte aequals,ineIS NOT NULL.
Exemplo
Query
EXPLAIN indexes = 1 para verificar se o skip index está sendo usado. Quando um caminho existe apenas em uma parte, o índice ignora a outra parte:
Query
Response
Query
Response
IS NOT NULL também usa o índice — ele ignora os grânulos em que o caminho não existe (já que o valor seria NULL):
Query
Response
Como funciona
JSONAllPaths(json_column) produz um Array(String) que contém todos os caminhos presentes em um valor JSON.
O skip index armazena essas strings de caminho em sua estrutura de dados (filtro de Bloom ou índice invertido).
Quando uma consulta filtra por json.some.path, o índice verifica se a string "some.path" está presente no índice de cada grânulo e ignora os grânulos em que ela está ausente.
Segurança com caminhos ausentes
NULLpara o tipoDynamic(por exemplo,json.path) e subcolunas tipadas comoNullable(por exemplo,json.path.:Int64) — comparações comNULLsempre retornam falso, portanto o skipping é seguro.- O valor padrão do tipo para expressões
CASTnãoNullable(por exemplo,json.path::Int64produz0quando o caminho está ausente) — o skipping é seguro apenas quando o valor comparado difere do valor padrão. O índice lida automaticamente com essa distinção.
Busca de texto completo com JSONAllValues
JSONAllValues.
JSONAllValues retorna todos os valores de uma coluna JSON como Array(String), que pode ser indexado com um índice de texto.
Um único índice em JSONAllValues(json_column) cobre todos os caminhos JSON, permitindo a busca de texto completo em qualquer subcoluna sem criar índices separados para cada caminho.
Consulte Índices baseados em valores com JSONAllValues na documentação sobre índices de texto para ver detalhes e exemplos.
Dicas para melhorar o uso do tipo JSON
JSON e carregar dados nela, considere as seguintes dicas:
- Analise seus dados e especifique o máximo possível de caminhos com tipos. Isso tornará o armazenamento e a leitura muito mais eficientes.
- Pense em quais caminhos você precisará e quais nunca precisará. Especifique os caminhos de que você não precisará na seção
SKIPe, se necessário, na seçãoSKIP REGEXP. Isso melhorará o armazenamento. - Não defina o parâmetro
max_dynamic_pathscom valores muito altos, pois isso pode tornar o armazenamento e a leitura menos eficientes. Embora isso dependa muito de parâmetros do sistema, como memória, CPU etc., uma regra geral é não definirmax_dynamic_pathsacima de 10 000 para o armazenamento no sistema de arquivos local e 1024 para o armazenamento no sistema de arquivos remoto.