Descrição
A especificação oficial completa do formatoNative está disponível aqui, e a especificação complementar do protocolo Native — o protocolo TCP wire que o transporta — está disponível aqui.
Ambas as especificações foram geradas por LLMs a partir do código-fonte do ClickHouse. O código continua sendo a principal fonte de referência: quando a especificação e o código divergem, o código está correto.
Native é o formato mais eficiente do ClickHouse porque é de fato “colunar”,
ou seja, não converte colunas em linhas.
Nesse formato, os dados são gravados e lidos em blocos, em formato binário.
Para cada bloco, são registrados, um após o outro, o número de linhas, o número de colunas, os nomes e tipos das colunas e as partes das colunas no bloco.
Esse é o formato usado na interface nativa para a interação entre servidores, no cliente de linha de comando e em clientes C++.
Formato wire dos tipos de dados
Os dados são enviados via wire em formato colunar, o que significa que cada coluna é enviada separadamente, e todos os valores de uma coluna são enviados juntos como um único array. Cada coluna em um bloco contém um cabeçalho semelhante a RowBinaryWithNamesAndTypes.Ao usar o protocolo binário TCP nativo (ou quando o endpoint HTTP recebe
?client_protocol_version=<n>),
uma estrutura BlockInfo é gravada antes das contagens de colunas e linhas. Os exemplos nesta seção usam
a interface HTTP simples, sem versão de protocolo, o que omite BlockInfo.Estrutura do bloco
A consulta a seguir retorna duas colunas,number e str, com três linhas:
Múltiplos blocos
No entanto, em muitos casos, os dados não cabem em um único bloco, e o ClickHouse os enviará em vários blocos. Considere a seguinte consulta, que busca duas linhas com o tamanho do bloco reduzido para forçar a divisão dos dados em uma linha por bloco:Tipos de dados simples
O formato wire de um valor individual de um dos tipos de dados mais simples é semelhante ao deRowBinary/RowBinaryWithNamesAndTypes.
A lista completa de tipos que correspondem a essa descrição inclui:
- (U)Int8, (U)Int16, (U)Int32, (U)Int64, (U)Int128, (U)Int256
- Float32, Float64
- Bool
- String
- FixedString(N)
- Date
- Date32
- DateTime
- DateTime64
- IPv4
- IPv6
- UUID
Tipos de dados complexos
A codificação dos tipos abaixo é diferente da deRowBinary e RowBinaryWithNamesAndTypes.
- Nullable
- LowCardinality
- Array
- Map
- Variant
- Dynamic
- JSON
Nullable
No formatoNative, uma coluna Nullable terá uma quantidade de bytes igual ao número de linhas no bloco antes dos dados propriamente ditos. Cada um desses bytes indica se o valor é NULL ou não. Por exemplo, com esta consulta, cada número ímpar será NULL:
Nullable(String). O indicador de nulo sempre vem do byte da máscara de nulabilidade —
um valor de máscara 0x01 significa que a linha é NULL, independentemente do conteúdo da string. Para linhas NULL,
a string subjacente é armazenada como uma string vazia (comprimento LEB128 0). Observe que uma string vazia não NULL
também tem comprimento LEB128 0; portanto, apenas o byte da máscara distingue os dois casos. Por exemplo, a seguinte consulta:
LowCardinality
Diferentemente do RowBinary, em queLowCardinality é transparente, o formato Native usa uma codificação colunar baseada em dicionário. Uma coluna é codificada com um prefixo de versão, seguido de um dicionário de valores únicos e de um array de índices inteiros nesse dicionário.
Uma coluna pode ser definida como
LowCardinality(Nullable(T)), mas não é possível defini-la como Nullable(LowCardinality(T)) — isso sempre resultará em um erro do servidor.UInt64(LE) com valor 1, gravado uma vez por coluna. Em seguida, por bloco, é gravado o seguinte:
UInt64(LE)— campo de bitsIndexesSerializationType. Os bits 0–7 codificam a largura do índice (0 = UInt8, 1 = UInt16, 2 = UInt32, 3 = UInt64). O bit 8 (NeedGlobalDictionaryBit) nunca é definido no formato Native (o servidor gera uma exceção se ele for encontrado). O bit 9 indica que há chaves adicionais de dicionário. O bit 10 indica que o dicionário deve ser reinicializado.UInt64(LE)— número de chaves do dicionário, seguido pelas chaves serializadas em lote usando a codificação do tipo interno.UInt64(LE)— número de linhas, seguido pelos valores de índice serializados em lote usando a largura UInt apropriada.
String, 0 para tipos numéricos). Para LowCardinality(Nullable(T)), o índice 0 representa NULL, e as chaves são serializadas sem o wrapper Nullable.
Por exemplo, LowCardinality(String) com 5 linhas ['foo', 'bar', 'baz', 'foo', 'bar']:
LowCardinality(Nullable(String)), o índice 0 é NULL:
Array
Ao contrário de RowBinary, em que cada array é precedido por uma contagem de elementos em LEB128, o formato Native codifica arrays como dois subfluxos colunares:- N offsets cumulativos
UInt64(little-endian, 8 bytes cada). A linhaitemoffset[i] - offset[i-1]elementos, comoffset[-1]implicitamente igual a 0. - Todos os elementos aninhados de todas as linhas, serializados em bloco de forma contígua.
Array(UInt32) com 3 linhas [[0, 10], [1, 11], [2, 12]]:
Array(String) com 4 linhas [[], ['0'], ['0','1'], ['0','1','2']]:
Map
UmMap(K, V) é codificado como Array(Tuple(K, V)) — offsets do array seguidos por todas as chaves e, em seguida, todos os valores. Isso difere de RowBinary, em que chaves e valores são intercalados em cada entrada.
Por exemplo, Map(String, UInt64) com 3 linhas [{'a':0,'b':10}, {'a':1,'b':11}, {'a':2,'b':12}]:
Variant
Ao contrário de RowBinary, em que cada linha traz seu próprio byte discriminante seguido do valor inline, o formato Native separa os discriminantes dos dados. Uma colunaVariant é codificada da seguinte forma:
- Prefixo do modo dos discriminantes
UInt64(LE)(0= BASIC,1= COMPACT). A saída do formato Native normalmente usa BASIC (0); o modo COMPACT pode aparecer ao ler dados armazenados comuse_compact_variant_discriminators_serializationativado. - N discriminantes
UInt8, um por linha. - Os dados de cada tipo variante em uma coluna em bloco separada, contendo apenas as linhas correspondentes, na ordem dos discriminantes.
Variant(String, UInt32) com 5 linhas [0::UInt32, 'hello', NULL, 3::UInt32, 'hello'] (ordenado: String = 0, UInt32 = 1):
Dynamic
Ao contrário de RowBinary, em que cada valor é autodescritivo (prefixo do tipo + valor), o formato Native serializaDynamic como um prefixo de estrutura seguido de uma coluna Variant.
O prefixo de estrutura contém uma versão de serialização UInt64(LE), seguida pelo número de tipos dinâmicos (como VarUInt) e, então, pelos nomes dos tipos como strings. Na versão V1, a contagem de tipos é gravada duas vezes por compatibilidade. Os dados que vêm na sequência formam uma coluna Variant cuja lista de tipos inclui os tipos dinâmicos e um tipo interno SharedVariant, ordenados alfabeticamente.
Por exemplo, Dynamic com 5 linhas [0::UInt32, 'hello', NULL, 3::UInt32, 'hello']:
JSON
Ao contrário do RowBinary, em que cada linha é autodescritiva com nomes de caminhos e valores, o formato Native serializaJSON em uma estrutura colunar. A codificação é complexa e depende da versão: consiste em um prefixo de estrutura com a versão de serialização, nomes de caminhos dinâmicos e o layout de dados compartilhados, seguido por caminhos tipados (cada um como uma coluna em bloco), caminhos dinâmicos (cada um como uma coluna Dynamic) e dados compartilhados para caminhos de overflow.
Para uma interoperabilidade mais simples, considere usar a configuração output_format_native_write_json_as_string=1, que serializa colunas JSON como strings simples de texto JSON (uma String por linha).