2 pontos por GN⁺ 2024-10-23 | Ainda não há comentários. | Compartilhar no WhatsApp
  • 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
    • Ex.: C.Int64

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
    • Ex.: C.Int64

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
    • Ex.: C.a.b
  • 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
    • Ex.: C.a.d.:Int64
  • Objetos JSON aninhados podem ser lidos como subcolunas do tipo JSON com a sintaxe JSON_column.^some.path
    • Ex.: C.^a
  • 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.

Ainda não há comentários.