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
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:
tags e ver que o JSON foi inserido como string:
JSONExtract podem ser usadas para extrair valores desse JSON. Considere o exemplo simples abaixo:
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.
JSONAsString:
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
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, masvisitParamHas('{"\\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.
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 tipoMap. 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 umMap é 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):
map, por exemplo:
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 tipoMap 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:
Map(String, Tuple(name String, time DateTime)), como mostrado abaixo:
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 aTuple 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:
request como Nested. Assim como em Tuple, é necessário especificar as subcolunas.
flatten_nested
A configuraçãoflatten_nested controla o comportamento do tipo Nested.
flatten_nested=1
Um valor de1 (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:
-
Precisamos usar a configuração
input_format_import_nested_jsonpara inserir o JSON como uma estrutura aninhada. Sem isso, é necessário achatar o JSON, ou seja: -
Os campos aninhados
method,patheversionprecisam ser enviados como arrays JSON, ou seja:
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 deTuples — 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:
-
input_format_import_nested_jsonnão é necessário para fazer insert. -
O tipo
Nestedé preservado emSHOW CREATE TABLE. Na prática, essa coluna é efetivamente umArray(Tuple(Nested(method LowCardinality(String), path String, version LowCardinality(String)))) -
Como resultado, é necessário fazer insert de
requestcomo um array, ou seja:
Exemplo
Uma versão mais completa dos dados acima está disponível em um bucket público no S3, em:s3://datasets-documentation/http/.
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:
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:JSONExtractKeysAndValues para isso:
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.