- O ClickHouse introduziu um novo tipo JSON que coloca os valores de cada caminho do JSON em um armazenamento verdadeiramente colunar, para evitar o gargalo de inserir documentos JSON como strings e fazer parse a cada leitura
- O núcleo da implementação são os tipos Variant e Dynamic, e mesmo que o mesmo caminho JSON receba tipos diferentes, como inteiro, string e array, eles não são forçados a uma unificação artificial em um tipo comum mínimo
- O valor padrão de
max_dynamic_paths é 1024 e o de max_dynamic_types é 32, limitando a quantidade de subcolunas e arquivos por tipo para controlar o aumento de descritores de arquivo e do custo de merge
- É possível ajustar a forma de armazenamento por caminho com dicas de tipo,
SKIP e SKIP REGEXP, e os valores podem ser lidos com a sintaxe de subcoluna como C.a.b
- O novo tipo pretende substituir o
Object('json') descontinuado, e o roadmap ainda inclui melhorias para usar caminhos de chave JSON em chave primária ou em data-skipping indexes
O desafio de adaptar JSON ao armazenamento colunar
- JSON é usado como formato comum para lidar com dados semiestruturados e não estruturados em logs, observabilidade, streaming de dados em tempo real, armazenamento de apps móveis e pipelines de machine learning
- O ClickHouse é um banco de dados verdadeiramente orientado a colunas, que armazena tabelas como um conjunto de arquivos de dados de colunas em disco para realizar compressão e filtros/agregações vetorizados
- Para obter o mesmo desempenho com JSON, em vez de guardar os documentos em uma coluna string e fazer parse depois, é preciso armazenar os valores de cada caminho JSON único como colunas
Quatro restrições tratadas pelo novo tipo JSON
-
Armazenamento colunar por caminho
- Os valores de cada caminho JSON também precisam poder ser comprimidos e filtrados/agregados de forma vetorizada, como colunas normais de tipos numéricos
-
Tipos que mudam dinamicamente
- O mesmo caminho JSON
a pode receber tipos diferentes, como inteiro, ponto flutuante e array
- O ClickHouse não consegue saber isso de antemão e os tipos podem nem ser compatíveis entre si, então uni-los em um tipo comum mínimo pode causar perda de informação
-
Prevenção de explosão de arquivos de coluna
- Se um novo arquivo de coluna for criado para cada novo caminho JSON, a quantidade de arquivos em disco cresce rapidamente em dados com muitas chaves únicas
- Descritores de arquivo consomem memória, e um volume maior de arquivos a processar também afeta o desempenho de merge
-
Armazenamento denso de chaves esparsas
- Quando há muitas chaves JSON únicas porém esparsas, não se deve armazenar repetidamente
NULL ou valores padrão em cada linha sem valor
- Apenas os valores reais precisam ser armazenados de forma densa para escalar até análises em escala de PB
Tipo Variant: a base para não unificar tipos à força
- O tipo de dados Variant é um recurso independente que pode ser usado fora do contexto de JSON, permitindo armazenar e ler valores de diferentes tipos de dados em uma mesma coluna da tabela
- As colunas tradicionais do ClickHouse têm tipo fixo, e os valores inseridos precisam corresponder a esse tipo ou ser convertidos implicitamente
- Colunas
Nullable usam um arquivo de máscara NULL além do arquivo de valores
Array armazena os tamanhos dos arrays em um arquivo separado e usa isso para calcular offsets
- Uma coluna Variant armazena valores do mesmo tipo concreto em subcolunas separadas por tipo
- Ex.: todos os valores
Int64 ficam em C.Int64.bin, e todos os String em C.String.bin
- O tipo usado em cada linha é rastreado por uma coluna discriminator
UInt8
- O valor do discriminator é o índice em uma lista ordenada de nomes de tipos
- O discriminator
255 é reservado para NULL
- Por esse desenho, Variant pode ter no máximo 255 tipos concretos
- Os arquivos de dados por tipo usam uma estrutura de armazenamento denso que contém apenas as linhas com valor
- Os arquivos por tipo não armazenam valores
NULL
- Para encontrar a posição da linha no arquivo do tipo real a partir da linha do discriminator, usa-se uma coluna de offsets
UInt64 em memória
- Esse offset não é gravado em disco e pode ser gerado sob demanda a partir do arquivo da coluna discriminator
- Variant suporta aninhamento arbitrário
- A ordem de tipos em
Variant(T1, T2) e Variant(T2, T1) tem o mesmo significado
- Também é possível colocar Variant dentro de Variant
- Valores de um tipo aninhado específico são lidos adicionando o nome do tipo como subcoluna
Tipo Dynamic: armazenar sem conhecer antes a lista de tipos
- O tipo Dynamic é um recurso independente implementado sobre Variant e também pode ser usado fora do contexto de JSON
- Dynamic adiciona duas capacidades ao Variant
- Permite armazenar valores de tipos arbitrários em uma única coluna sem precisar especificar antes a lista de tipos
- Permite limitar quantos tipos serão armazenados em arquivos de dados de coluna separados
- O armazenamento interno é igual ao do Variant, mas com o arquivo adicional
C.dynamic_structure.bin
- Esse arquivo contém a lista de tipos armazenados como subcolunas e estatísticas de tamanho dos arquivos de dados por tipo
- Esses metadados são usados na leitura de subcolunas e em merges de partes de dados
Dynamic(max_types=N) limita a quantidade de tipos armazenados em arquivos separados
0 <= N < 255
- O valor padrão é 32
- Quando o limite é atingido, os valores dos demais tipos são armazenados em um único arquivo de coluna como
C.SharedVariant.bin
- O tipo desse arquivo é
String
- Cada linha contém um valor string na estrutura
<binary_encoded_data_type><binary_value>
- Isso permite armazenar e reler valores de vários tipos dentro de um único arquivo de coluna
- Dynamic também usa nomes de tipo como subcolunas, como Variant, para ler valores de um tipo específico
Declaração do tipo JSON e estrutura de armazenamento
- O novo tipo JSON armazena objetos JSON de estrutura arbitrária e permite ler cada valor JSON como subcolunas baseadas em caminho
- A declaração do tipo pode ter parâmetros opcionais e dicas
<column_name> JSON(
max_dynamic_paths=N,
max_dynamic_types=M,
some.path TypeName,
SKIP path.to.skip,
SKIP REGEXP 'paths_regexp')
max_dynamic_paths
- O valor padrão é 1024
- Define quantos caminhos de chave JSON serão armazenados como subcolunas separadas
- Caminhos que ultrapassarem o limite serão armazenados juntos em uma única subcoluna com estrutura especial
max_dynamic_types
- O valor padrão é 32
- O intervalo de valores vai de
0 a 254
- Define quantos tipos de dados de uma coluna de caminho de chave JSON serão armazenados em arquivos de dados de coluna separados
- Novos tipos que ultrapassarem o limite serão armazenados juntos em um único arquivo de dados de coluna com estrutura especial
some.path TypeName
- É uma dica de tipo para um caminho JSON específico
- Esse caminho sempre será armazenado como subcoluna do tipo especificado, oferecendo garantia de desempenho
SKIP path.to.skip
- Ignora um caminho JSON específico durante o parsing
- Esse caminho não é armazenado na coluna JSON
- Se o caminho especificado for um objeto JSON aninhado, todo o objeto aninhado será ignorado
SKIP REGEXP 'path_regexp'
- Ignora, durante o parsing do JSON, caminhos que correspondam à expressão regular
- Caminhos correspondentes não são armazenados na coluna JSON
Como ler caminhos JSON como colunas
- O valor de cada caminho leaf único da coluna JSON é armazenado em disco de uma de duas formas
- Caminhos com dica de tipo são armazenados como arquivos normais de dados de coluna
- Caminhos cujo tipo pode mudar dinamicamente são armazenados como subcolunas Dynamic
- O tipo JSON usa um arquivo especial chamado
object_structure
- Ele guarda metadados sobre caminhos dinâmicos
- Guarda estatísticas de valores não nulos de cada caminho dinâmico
- É usado para leitura de subcolunas e merges de partes de dados
- A explosão de arquivos de coluna é controlada com limites em dois níveis
max_dynamic_types limita quantos tipos dentro de um caminho de chave JSON serão armazenados em arquivos separados
max_dynamic_paths limita quantos caminhos de chave JSON serão armazenados como subcolunas separadas
- Caminhos JSON dinâmicos adicionais que ultrapassarem o limite de
max_dynamic_paths serão armazenados como shared data
- Exemplos de arquivos:
C.object_shared_data.size0.bin, C.object_shared_data.paths.bin, C.object_shared_data.values.bin
object_shared_data.values é do tipo String
- Cada entrada tem a estrutura
<binary_encoded_data_type><binary_value>
- Estatísticas adicionais também são gravadas em
object_structure.bin para os shared data
- Atualmente são armazenadas estatísticas de valores não nulos para os primeiros 10000 caminhos presentes na coluna de shared data
Sintaxe de caminhos JSON e objetos aninhados
- O tipo JSON permite ler o valor leaf de cada caminho como subcoluna baseada no nome do caminho
- Valores de caminhos sem dica de tipo sempre têm tipo Dynamic
- Ex.: o tipo de
C.a.d é Dynamic
- Subcolunas de subtipo do tipo Dynamic são lidas com uma sintaxe JSON especial
- Objetos JSON aninhados podem ser lidos como subcolunas do tipo JSON com a sintaxe
JSON_column.^some.path
- Atualmente, por motivos de desempenho, a sintaxe com ponto não lê objetos aninhados
- A estrutura atual de armazenamento é eficiente para ler valores literais por caminho
- Ler o subobjeto completo de um caminho exigiria ler mais dados e poderia ser mais lento
- Para retornar objetos, é necessária a sintaxe
.^
- O ClickHouse planeja unificar as duas sintaxes
. no futuro
Serialização compacta de discriminator
- Muitos caminhos JSON dinâmicos podem ter, na maior parte do tempo, o mesmo tipo de valor
- Quando existem muitos caminhos JSON únicos porém esparsos, os arquivos de discriminator de cada caminho tendem a conter principalmente
255, ou seja, NULL
- Esses arquivos comprimem bem, mas ainda assim há muita redundância quando todos os valores da linha são iguais
- O ClickHouse implementou um formato compacto para serialização de discriminator
- Em vez de gravar normalmente todos os valores
UInt8 do discriminator, se todos os discriminators do granule alvo forem iguais, apenas 3 valores são serializados
- Indicador de formato compacto do granule
- Indicador da quantidade de valores naquele granule
- Valor do discriminator
- Essa otimização é controlada pela configuração MergeTree
use_compact_variant_discriminators_serialization
- Ela vem ativada por padrão
- Em vez de armazenar 8192 valores com a granulosidade de índice padrão, em alguns casos passam a ser armazenados apenas 3 valores
Status de release e próximos passos
- O novo tipo JSON foi projetado para substituir o tipo Object('json'), que foi descontinuado
- A implementação foi disponibilizada como recurso experimental para testes no release 24.08 do ClickHouse
- O roadmap de JSON inclui melhorias para usar caminhos de chave JSON na chave primária da tabela ou em data-skipping indexes
- Componentes como Variant e Dynamic também servem de base para oferecer suporte a outros tipos semiestruturados, como XML e YAML
- Usuários do ClickHouse Cloud que quiserem testar o novo tipo de dados JSON devem entrar em contato com a equipe de suporte do ClickHouse para solicitar acesso ao private preview
Ainda não há comentários.