Skip to main content
Veja a seguir alternativas para modelar JSON no ClickHouse. Elas estão documentadas por completude e eram aplicáveis antes do desenvolvimento do tipo JSON; por isso, em geral não são recomendadas nem se aplicam à maioria dos casos de uso.
Adote uma abordagem no nível do objetoTécnicas diferentes podem ser aplicadas a objetos diferentes no mesmo esquema. Por exemplo, alguns objetos podem ser melhor representados com o tipo String, e outros com o tipo Map. Observe que, depois que um tipo String é usado, não é mais necessário tomar outras decisões de esquema. Por outro lado, é possível aninhar subobjetos dentro de uma chave Map — incluindo uma String que representa JSON — como mostramos abaixo:

Usando o tipo String

Se os objetos forem altamente dinâmicos, sem uma estrutura previsível e contiverem objetos aninhados arbitrários, você deve usar o tipo String. Os valores podem ser extraídos em tempo de consulta usando funções JSON, como mostramos abaixo. Lidar com dados usando a abordagem estruturada descrita acima muitas vezes não é viável para usuários com JSON dinâmico, sujeito a mudanças ou cujo esquema não é bem compreendido. Para ter total flexibilidade, você pode simplesmente armazenar o JSON como Strings e depois usar funções para extrair os campos conforme necessário. Isso representa o extremo oposto de tratar JSON como um objeto estruturado. Essa flexibilidade tem um custo e traz desvantagens significativas — principalmente o aumento da complexidade da sintaxe da consulta, além da piora no desempenho. Como observado anteriormente, para o objeto person original, não podemos garantir a estrutura da coluna tags. Inserimos a linha original (incluindo company.labels, que ignoramos por enquanto), declarando a coluna Tags como String:
Podemos selecionar a coluna tags e ver que o JSON foi inserido como string:
As funções JSONExtract podem ser usadas para extrair valores desse JSON. Considere o exemplo simples abaixo:
Observe que as funções exigem tanto uma referência à coluna String tags quanto um path no JSON a ser extraído. Paths aninhados exigem o aninhamento das funções, por exemplo, JSONExtractUInt(JSONExtractString(tags, 'car'), 'year'), que extrai a coluna tags.car.year. A extração de paths aninhados pode ser simplificada por meio das funções JSON_QUERY e JSON_VALUE. Considere o caso extremo do dataset arxiv, em que o corpo inteiro é tratado como uma String.
Para inserir nesse esquema, precisamos usar o formato JSONAsString:
Suponha que desejamos contar o número de artigos publicados por ano. Compare a consulta a seguir, usando apenas String, com a versão estruturada do esquema:
Observe o uso de uma expressão XPath aqui para filtrar o JSON por method, isto é, JSON_VALUE(body, '$.versions[0].created'). As funções de String são significativamente mais lentas (> 10x) do que conversões explícitas de tipo com índices. As consultas acima sempre exigem uma varredura completa da tabela e o processamento de todas as linhas. Embora essas consultas ainda sejam rápidas em um conjunto de dados pequeno como este, o desempenho se degradará em conjuntos de dados maiores. A flexibilidade dessa abordagem tem um custo claro em desempenho e sintaxe, e ela deve ser usada apenas para objetos altamente dinâmicos no esquema.

Funções JSON simples

Os exemplos acima usam a família de funções JSON*. Elas utilizam um parser JSON completo baseado em simdjson, que faz uma análise rigorosa e distingue o mesmo campo aninhado em níveis diferentes. Essas funções conseguem lidar com JSON sintaticamente correto, mas mal formatado, por exemplo, com espaços duplos entre chaves. Também há disponível um conjunto de funções mais rápido e mais rigoroso. Essas funções simpleJSON* oferecem desempenho potencialmente superior, principalmente por fazerem suposições estritas sobre a estrutura e o formato do JSON. Especificamente:
  • Os nomes dos campos devem ser constantes
  • Codificação consistente dos nomes dos campos, por exemplo simpleJSONHas('{"abc":"def"}', 'abc') = 1, mas visitParamHas('{"\\u0061\\u0062\\u0063":"def"}', 'abc') = 0
  • Os nomes dos campos devem ser únicos em todas as estruturas aninhadas. Não é feita distinção entre níveis de aninhamento, e a correspondência é indiscriminada. Em caso de vários campos correspondentes, a primeira ocorrência é usada.
  • Nenhum caractere especial fora de literais de string. Isso inclui espaços. O exemplo a seguir é inválido e não será analisado.
Já o exemplo a seguir será analisado corretamente:
A consulta acima usa simpleJSONExtractString para extrair a chave created, aproveitando o fato de que queremos apenas o primeiro valor da data de publicação. Neste caso, as limitações das funções simpleJSON* são aceitáveis em troca do ganho de desempenho.

Usando o tipo Map

Se o objeto for usado para armazenar chaves arbitrárias, em sua maioria de um único tipo, considere usar o tipo Map. Idealmente, o número de chaves únicas não deve exceder algumas centenas. O tipo Map também pode ser considerado para objetos com subobjetos, desde que estes tenham uniformidade de tipos. Em geral, recomendamos usar o tipo Map para labels e tags, por exemplo, labels de pod do Kubernetes em dados de log. Embora Maps ofereçam uma forma simples de representar estruturas aninhadas, eles têm algumas limitações importantes:
  • Os campos devem ser todos do mesmo tipo.
  • O acesso a subcolunas exige uma sintaxe especial de map, já que os campos não existem como colunas. O objeto inteiro é uma coluna.
  • Ao acessar uma subcoluna, todo o valor do Map é carregado, ou seja, todos os elementos irmãos e seus respectivos valores. Em maps maiores, isso pode resultar em uma perda significativa de desempenho.
Chaves StringAo modelar objetos como Maps, usa-se uma chave String para armazenar o nome da chave JSON. Portanto, o map sempre será Map(String, T), em que T depende dos dados.

Valores primitivos

A aplicação mais simples de um Map é quando o objeto contém valores do mesmo tipo primitivo. Na maioria dos casos, isso envolve usar o tipo String para o valor T. Considere nosso JSON de pessoa anterior, em que se determinou que o objeto company.labels era dinâmico. É importante destacar que esperamos que apenas pares chave-valor do tipo String sejam adicionados a esse objeto. Assim, podemos declará-lo como Map(String, String):
Podemos inserir o objeto JSON completo original:
Para consultar esses campos dentro do objeto de requisição, é necessário usar a sintaxe de map, por exemplo:
Um conjunto completo de funções de Map está disponível para consultar esse tipo, conforme descrito aqui. Se os seus dados não forem de um tipo consistente, há funções para realizar a coerção de tipos necessária.

Valores de objeto

O tipo Map também pode ser considerado para objetos que têm subobjetos, desde que estes últimos mantenham consistência em seus tipos. Suponha que a chave tags do nosso objeto persons exija uma estrutura consistente, em que o subobjeto de cada tag tenha uma coluna name e time. Um exemplo simplificado desse tipo de documento JSON pode ser como o seguinte:
Isso pode ser representado com um Map(String, Tuple(name String, time DateTime)), como mostrado abaixo:
O uso de maps neste caso geralmente é incomum e sugere que os dados devem ser remodelados para que nomes de chave dinâmicos não tenham subobjetos. Por exemplo, o trecho acima poderia ser remodelado da seguinte forma, permitindo o uso de Array(Tuple(key String, name String, time DateTime)).

Usando o tipo Nested

O tipo Nested pode ser usado para modelar objetos estáticos que raramente sofrem alterações, oferecendo uma alternativa a Tuple e Array(Tuple). Em geral, recomendamos evitar o uso desse tipo para JSON, pois seu comportamento costuma ser confuso. O principal benefício de Nested é que sub-colunas podem ser usadas em chaves de ordenação. Abaixo, apresentamos um exemplo de uso do tipo Nested para modelar um objeto estático. Considere a seguinte entrada de log simples em JSON:
Podemos declarar a chave request como Nested. Assim como em Tuple, é necessário especificar as subcolunas.

flatten_nested

A configuração flatten_nested controla o comportamento do tipo Nested.

flatten_nested=1

Um valor de 1 (o padrão) não oferece suporte a um nível arbitrário de aninhamento. Com esse valor, é mais fácil entender uma estrutura de dados aninhada como várias colunas Array de mesmo comprimento. Na prática, os campos method, path e version são colunas Array(Type) separadas, com uma restrição crítica: o comprimento dos campos method, path e version deve ser o mesmo. Se usarmos SHOW CREATE TABLE, isso fica ilustrado:
A seguir, inserimos nesta tabela:
Alguns pontos importantes a considerar aqui:
  • Precisamos usar a configuração input_format_import_nested_json para inserir o JSON como uma estrutura aninhada. Sem isso, é necessário achatar o JSON, ou seja:
  • Os campos aninhados method, path e version precisam ser enviados como arrays JSON, ou seja:
As colunas podem ser consultadas usando notação por ponto:
Observe que o uso de Array para as subcolunas significa que toda a variedade de funções de array pode ser aproveitada, incluindo a cláusula ARRAY JOIN — útil se suas colunas tiverem vários valores.

flatten_nested=0

Isso permite um nível arbitrário de aninhamento e significa que as colunas aninhadas permanecem como um único array de Tuples — na prática, tornando-se equivalentes a Array(Tuple). Esta é a abordagem preferida e, muitas vezes, a mais simples de usar JSON com Nested. Como mostramos abaixo, ela exige apenas que todos os objetos estejam em uma lista. Abaixo, recriamos nossa tabela e inserimos novamente uma linha:
Alguns pontos importantes a observar aqui:
  • input_format_import_nested_json não é necessário para fazer insert.
  • O tipo Nested é preservado em SHOW CREATE TABLE. Na prática, essa coluna é efetivamente um Array(Tuple(Nested(method LowCardinality(String), path String, version LowCardinality(String))))
  • Como resultado, é necessário fazer insert de request como um array, ou seja:
As colunas podem novamente ser consultadas usando notação por ponto:

Exemplo

Uma versão mais completa dos dados acima está disponível em um bucket público no S3, em: s3://datasets-documentation/http/.
Dadas as restrições e o formato de entrada do JSON, inserimos este conjunto de dados de exemplo com a seguinte consulta. Aqui, definimos flatten_nested=0. A instrução a seguir insere 10 milhões de linhas, portanto pode levar alguns minutos para ser executada. Aplique um LIMIT, se necessário:
Consultar esses dados exige acessar os campos da requisição como arrays. Abaixo, resumimos os erros e os métodos HTTP em um período fixo.

Usando arrays pareados

Arrays pareados oferecem um equilíbrio entre a flexibilidade de representar JSON como Strings e o desempenho de uma abordagem mais estruturada. O esquema é flexível no sentido de que novos campos podem ser adicionados à raiz. Isso, no entanto, exige uma sintaxe de consulta significativamente mais complexa e não é compatível com estruturas aninhadas. Como exemplo, considere a tabela a seguir:
Para inserir dados nesta tabela, precisamos estruturar o JSON como uma lista de chaves e valores. A consulta a seguir ilustra o uso de JSONExtractKeysAndValues para isso:
Observe como a coluna request continua sendo uma estrutura aninhada representada como uma string. Podemos inserir novas chaves na raiz livremente. Também podemos ter diferenças arbitrárias no próprio JSON. Para inserir na nossa tabela local, execute o seguinte:
Para consultar essa estrutura, é preciso usar a função indexOf para identificar o índice da chave necessária (que deve ser consistente com a ordem dos valores). Isso pode ser usado para acessar a coluna de array values, ou seja, values[indexOf(keys, 'status')]. Ainda precisamos de um método de parsing de JSON para a coluna request — neste caso, simpleJSONExtractString.
Última modificação em 3 de julho de 2026