> ## Documentation Index
> Fetch the complete documentation index at: https://clickhouse.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

> Especificação do formato colunar Native do ClickHouse: primitivas do formato wire, a estrutura de bloco e Column, a codificação de cada tipo de dado e o quadro de compressão

# Formato Native

O formato Native é o formato wire colunar que o ClickHouse usa para transferir dados tabulares. Ele é usado em vários contextos:

* no corpo dos pacotes `Data`, `Totals`, `Extremes`, `Log` e `ProfileEvents` do [protocolo TCP nativo](https://clickhouse.com/docs/pt-BR/resources/develop-contribute/native-protocol/protocol.md) (o pacote `TableColumns` **não** é um bloco Native — ele transporta duas strings binárias, portanto seu layout é descrito na [especificação do protocolo nativo](https://clickhouse.com/docs/pt-BR/resources/develop-contribute/native-protocol/protocol.md));
* na saída de `SELECT ... FORMAT Native` via HTTP;
* em arquivos exportados com `INTO OUTFILE ... FORMAT Native`;
* nas cargas úteis de replicação entre servidores.

Esta página descreve os bytes contidos em um bloco — a carga útil colunar — e as codificações de tipo por coluna que o compõem. O enquadramento de pacotes, o estado da conexão e a negociação de versão são tratados na [especificação do protocolo nativo](https://clickhouse.com/docs/pt-BR/resources/develop-contribute/native-protocol/protocol.md).

Todos os campos inteiros de múltiplos bytes são little-endian. Inteiros com sinal usam complemento de dois.

> [!TIP]
>
> Para uma introdução ao formato `Native` voltada ao usuário (com exemplos de `curl`), consulte a [página do formato Native](https://clickhouse.com/docs/pt-BR/reference/formats/Native.md). Esta especificação é a referência de baixo nível do formato wire.

## Visão geral

Tudo o que transporta linhas no wire é um **bloco**: um fragmento autodescritivo de linhas armazenadas coluna por coluna. Primeiro vêm todos os valores da coluna 1, depois todos os da coluna 2 e assim por diante. Um bloco transporta apenas as colunas referenciadas pela consulta, nunca a tabela inteira.

O `data` de uma coluna é organizado de acordo com a *família* à qual seu tipo pertence. As famílias, em ordem crescente de complexidade do decodificador, são:

![Estrutura de um bloco wire colunar e suas famílias de layout de dados](https://mintcdn.com/private-7c7dfe99/kA9td9fgr8TRWXKS/images/interfaces/native-format/block-structure.webp?fit=max\&auto=format\&n=kA9td9fgr8TRWXKS\&q=85\&s=e4e067d9d428c13b9fce7fa7fe812828)

* Tipos de **largura fixa** organizam `data` como `bytes_per_value × num_rows` bytes brutos, sem delimitação por linha.
* Tipos **compostos** (`Nullable`, `Array`, `Tuple`, `Map`, `Nested`) têm uma estrutura recursiva totalmente derivável da string do tipo, sem prefixo de versão e sem estado entre blocos.
* Tipos **versionados / com estado** (`LowCardinality`, `JSON`, `Variant`, `Dynamic`) iniciam cada bloco não vazio com um prefixo de versão de serialização/estado. No wire `Native`, esse prefixo e qualquer dicionário são **por bloco**: o formato não mantém estado *entre* blocos (o componente de gravação cria um novo estado de serialização para cada bloco e define `low_cardinality_max_dictionary_size = 0`). O estado entre blocos diz respeito ao armazenamento em disco do MergeTree, não ao layout wire do Native.

## Primitivas de wire

O formato Native se baseia em quatro codificações primitivas.

| Primitiva | Tamanho | Descrição |
| - | - | - |
| VarUInt | 1–10 B | Inteiro sem sinal de comprimento variável LEB-128 |
| Int de largura fixa | 1, 2, 4, 8, 16, 32 B | Little-endian, complemento de dois para tipos com sinal |
| String | variável | Prefixo de comprimento VarUInt + bytes brutos por valor; um [stream de offsets](#string-type) separado a partir da revisão `54492` |
| Bool | 1 B | `0x00` = false, qualquer valor diferente de zero = true |

### VarUInt

Um inteiro sem sinal de comprimento variável que usa a codificação LEB-128. Cada byte contém 7 bits de dados nas posições 0–6 e 1 bit de continuação na posição 7. O bit de continuação é `1` quando há mais bytes em seguida e `0` no último byte.

| Intervalo de valores | Bytes |
| - | - |
| 0 – 127 | 1 |
| 128 – 16383 | 2 |
| 16384 – 2097151 | 3 |
| até o UInt64 completo | até 10 |

Codificação do valor `300`:

```text theme={null}
300 = 0b100101100

Byte 0: 0xAC = 0b10101100   (data: 0101100, continuation: 1)
Byte 1: 0x02 = 0b00000010   (data: 0000010, continuation: 0)
```

Decodificando os bytes `0xAC 0x02`:

```text theme={null}
Byte 0: data = 0x2C, continuation = 1 → accumulator = 0x2C, shift = 7
Byte 1: data = 0x02, continuation = 0 → accumulator = (0x02 << 7) | 0x2C = 300
```

### Inteiros de largura fixa

| Tipo | Bytes | Codificação |
| - | - | - |
| UInt8 | 1 | Byte bruto |
| UInt16 | 2 | Little-endian |
| UInt32 | 4 | Little-endian |
| UInt64 | 8 | Little-endian |
| UInt128 | 16 | Little-endian |
| UInt256 | 32 | Little-endian |
| Int8 | 1 | Byte bruto, complemento de dois |
| Int16 | 2 | Little-endian, complemento de dois |
| Int32 | 4 | Little-endian, complemento de dois |
| Int64 | 8 | Little-endian, complemento de dois |
| Int128 | 16 | Little-endian, complemento de dois |
| Int256 | 32 | Little-endian, complemento de dois |
| Float32 | 4 | IEEE 754 de precisão simples, little-endian |
| Float64 | 8 | IEEE 754 de precisão dupla, little-endian |

Por exemplo, o valor UInt32 `1` é codificado como `01 00 00 00`, e o valor Int32 `-1`, como `FF FF FF FF`.

### String

Uma sequência de bytes prefixada pelo seu comprimento:

```text theme={null}
[VarUInt: byte_length] [byte_length bytes: raw value]
```

A sequência de bytes não precisa ser UTF-8 válido. Uma string vazia é codificada como um único byte `0x00`, e as strings podem conter quaisquer valores de byte, inclusive caracteres NUL embutidos. A string `"ab"` é codificada como `02 61 62`; para decodificá-la, leia o comprimento VarUInt (`2`) e, em seguida, leia essa quantidade de bytes.

### Bool

Um único byte. `0x00` é false; qualquer valor diferente de zero é true (na forma canônica, `0x01`).

## Estrutura de blocos e colunas

### Layout wire do bloco

```text theme={null}
[BlockInfo]               metadata (only on the TCP Data-packet path; see below)
[VarUInt: num_columns]    number of columns in this block
[VarUInt: num_rows]       number of rows in this block
[Column × num_columns]    column entries, omitted when num_columns = 0
```

A presença do prefixo `BlockInfo` depende do canal, porque o mecanismo de gravação é parametrizado por uma *revisão* (consulte [Revisão do protocolo e o formato Native](#protocol-revision) para uma explicação completa, incluindo o fato de `client_protocol_version` afetar apenas a saída):

* No **protocolo TCP nativo**, o servidor grava blocos na revisão negociada da conexão (um valor alto — `DBMS_TCP_PROTOCOL_VERSION`, consulte `src/Core/ProtocolDefines.h`). O `BlockInfo` é gravado sempre que essa revisão for maior que zero, o que sempre acontece em uma conexão real. O byte `has_custom_serialization` em cada coluna (consulte [layout wire da coluna](#column-wire-layout)) é gravado a partir da revisão `54454`.
* O *formato de saída* `Native` — `SELECT ... FORMAT Native` via HTTP, `INTO OUTFILE ... FORMAT Native` e o formato `Native` produzido pelo `clickhouse-client` — serializa na revisão `0` *por padrão*. Na revisão `0`, tanto o prefixo `BlockInfo` quanto o byte `has_custom_serialization` são omitidos, de modo que um bloco consiste apenas em `num_columns`, `num_rows` e as colunas.

  Via HTTP, essa revisão não é fixa: um cliente pode aumentá-la com o parâmetro de consulta `?client_protocol_version=<n>`, e o servidor usa esse valor como revisão de serialização da resposta.

  Com um valor suficientemente alto, a saída HTTP inclui o prefixo `BlockInfo` (gravado sempre que a revisão for maior que `0`) e o byte `has_custom_serialization` (gravado a partir da revisão `54454`), exatamente como no caminho TCP. Portanto, os clientes não devem presumir que toda carga útil HTTP `FORMAT Native` esteja na revisão `0`.

Em outras palavras, os exemplos de bytes desta seção que começam com um prefixo `BlockInfo` descrevem a carga útil do pacote Data do TCP. A mesma consulta executada via `FORMAT Native` produz a forma mais curta mostrada ao lado de cada um deles.

### BlockInfo

BlockInfo é uma sequência de campos, cada um precedido por um ID de campo VarUInt e encerrada por um ID de campo `0`. O formato wire **não** é autodescritivo: um ID de campo não codifica o comprimento nem o tipo do seu valor, portanto o leitor precisa conhecer de antemão o tipo de cada ID de campo que possa encontrar. O próprio leitor do ClickHouse trata um ID de campo não reconhecido como corrupção e gera uma exceção (`UNKNOWN_BLOCK_INFO_FIELD`). A compatibilidade futura é garantida, em vez disso, pela revisão do protocolo: o remetente só grava um campo se a revisão negociada for igual ou superior à revisão mínima desse campo, de modo que um receptor mais antigo nunca recebe um campo que não conhece.

| ID do campo | Campo | Tipo | Revisão mínima | Descrição |
| - | - | - | - | - |
| 1 | is\_overflows | UInt8 | 0 | Bloco de overflow do GROUP BY. `0` para blocos sem overflow. |
| 2 | bucket\_number | Int32 | 0 | Bucket de agregação. `-1` para blocos sem bucket. |
| 3 | out\_of\_order\_buckets | Lista de Int32 | 54480 | Buckets atrasados durante a agregação distribuída. Codificado como uma contagem VarUInt seguida da mesma quantidade de valores `Int32`. |
| 0 | (terminador) | — | — | Fim do BlockInfo. Sempre obrigatório. |

Os campos `1` e `2` têm revisão mínima `0`, portanto estão presentes sempre que um `BlockInfo` é gravado. O campo `3` só é gravado a partir da revisão `54480`. Layout wire para o caso comum (revisão inferior a `54480`):

```text theme={null}
[VarUInt: 1] [UInt8: is_overflows]
[VarUInt: 2] [Int32: bucket_number]
[VarUInt: 0]
```

### Layout wire de coluna

Uma Column aparece `num_columns` vezes dentro de um bloco.

| # | Campo | Tipo | Condição | Descrição |
| - | - | - | - | - |
| 1 | name | String | sempre | Nome da coluna |
| 2 | type | String *ou* codificação binária de tipo | sempre | String de tipo do ClickHouse (por exemplo, `"UInt64"`, `"Array(String)"`) por padrão; uma codificação binária de tipo quando `output_format_native_encode_types_in_binary_format = 1`, exceto em uma consulta secundária (veja a nota abaixo) |
| 3 | has\_custom\_serialization | UInt8 | recurso `CUSTOM_SERIALIZATION` (v54454) | `0` = padrão, `1` = personalizada (seguido de kind\_stack) |
| 4 | kind\_stack | bytes | quando o campo 3 = `1` | Um byte de enum UInt8 (veja abaixo) que descreve a serialização diferente do padrão (esparsa etc.). Para o valor `COMBINATION`, é seguido por uma contagem VarUInt e pelo mesmo número de bytes de tipo adicionais. Para uma `Tuple` (e outros tipos compostos com informações de serialização por elemento), a carga útil é recursiva — veja abaixo. |
| 5 | data | bytes | sempre | Valores da coluna para todas as `num_rows` linhas. Layout por tipo — veja [tipos de dados](#data-types). Para colunas esparsas, veja abaixo. |

Um decodificador faz o encaminhamento com base na String de tipo `type`. As Strings de tipo costumam trazer parâmetros entre parênteses; o decodificador remove o sufixo `(...)` para identificar o tipo básico e, em seguida, interpreta os parâmetros para decidir tamanho, escala ou tipo interno. Interpretar uma lista de parâmetros com tipos aninhados (por exemplo, uma `Tuple` dentro de um `Array`) exige um divisor de vírgulas que leve em conta a profundidade e acompanhe o aninhamento de parênteses, em vez de simplesmente dividir por `,`.

> [!NOTE]
>
> **Codificação binária de tipo**
>
> O campo `type` é uma `String` textual apenas no modo padrão. Quando a configuração de consulta `output_format_native_encode_types_in_binary_format = 1` está definida, esse campo passa a ser uma **codificação binária de tipo** — a mesma codificação baseada em tags documentada em [codificação binária de tipos de dados](https://clickhouse.com/docs/pt-BR/reference/data-types/data-types-binary-encoding.md) — e as listas achatadas do tipo `Dynamic` usam a mesma codificação binária para os nomes de cada tipo. Um decodificador que sempre lê o campo 2 como uma string prefixada pelo comprimento interpretaria a primeira tag binária de tipo como o comprimento de uma string e perderia a sincronização; por isso, ele precisa saber qual modo o fluxo utiliza.
>
> Em uma consulta com `query_kind = SECONDARY_QUERY` (enviada por outro servidor para `remote`, `Distributed` ou réplicas paralelas, ou por `clickhouse-client --query_kind secondary_query`), ambos os lados ignoram `output_format_native_encode_types_in_binary_format` e `input_format_native_decode_types_in_binary_format` nos blocos dessa consulta, que transportam nomes de tipos textuais nas duas direções. Servidores anteriores a essa regra aplicam essas configurações a uma consulta secundária da mesma forma que a qualquer outra consulta.

![Encaminhamento do decodificador de uma descrição de tipo para sua família de codificação](https://mintcdn.com/private-7c7dfe99/kA9td9fgr8TRWXKS/images/interfaces/native-format/decoder-dispatch.webp?fit=max\&auto=format\&n=kA9td9fgr8TRWXKS\&q=85\&s=b922e9bfee45e9c3f8d7e090bc5a31ea)

#### kind\_stack e codificação esparsa

O byte `kind_stack` enumera uma serialização por coluna diferente do padrão:

| Byte | Nome | Significado | Impacto no formato wire de `data` |
| - | - | - | - |
| `0x00` | DEFAULT | Serialização padrão | Idêntico a `has_custom = 0` |
| `0x01` | SPARSE | Serialização esparsa (v54465+) | Fluxo de offsets + valores diferentes do padrão; veja abaixo |
| `0x02` | DETACHED | Coluna encapsulada em um `ColumnBLOB` pelo marshalling paralelo de bloco (v54478+) | Blob pré-serializado: `VarUInt size` + essa quantidade de bytes; veja abaixo |
| `0x03` | DETACHED\_OVER\_SPARSE | Uma coluna esparsa encapsulada em um `ColumnBLOB` | Mesma carga útil de blob que `DETACHED`; veja abaixo |
| `0x04` | REPLICATED | Forma de dicionário para valores repetidos (v54482+) | Fluxo de índices + valores densos de elementos; veja abaixo |
| `0x05` | COMBINATION | Pilha de múltiplos tipos | Seguido por um VarUInt `count` e por `count` bytes de tipo adicionais — veja a nota abaixo |

**A carga útil de `COMBINATION` usa um enum diferente.** As cinco linhas acima são códigos *compactos* de um byte. `COMBINATION` (`0x05`) é o escape genérico para qualquer pilha que eles não cobrem: é seguido por um `VarUInt` `count` e, depois, por `count` entradas de um byte. Essas entradas **não** são os códigos compactos da tabela — são os valores brutos de `ISerialization::Kind`:

| Byte | `Kind` aninhado |
| - | - |
| `0x00` | DEFAULT |
| `0x01` | SPARSE |
| `0x02` | DETACHED |
| `0x03` | REPLICATED |

Os valores dos bytes diferem dos códigos compactos: `REPLICATED` é `0x03` nesse enum aninhado, mas `0x04` como código compacto, e não existe entrada `DETACHED_OVER_SPARSE` — essa combinação aparece como duas entradas consecutivas, `SPARSE` e `DETACHED`. Um decodificador que continue usando a tabela compacta para os bytes aninhados vai mapear `0x03`/`0x04` incorretamente e perder a sincronização.

O `count` é o comprimento total da pilha, **incluindo a entrada `DEFAULT` inicial** com a qual toda pilha começa. Como os códigos compactos já cobrem todas as pilhas de uma e de duas entradas, um `COMBINATION` sempre tem `count` igual a pelo menos três.

**Validade da pilha de tipos.** Um decodificador rejeita uma pilha malformada com `INCORRECT_DATA` em vez de construir a serialização que ela descreve. Uma pilha válida atende às seguintes condições:

* a pilha começa com `DEFAULT`;
* a pilha é uma subsequência de `DEFAULT`, `SPARSE`, `REPLICATED`, `DETACHED` — a ordem em que os tipos encapsulam uns aos outros. `SPARSE` pode ficar dentro de `REPLICATED`, mas não o contrário, e `DETACHED` é sempre o último, pois um `ColumnBLOB` contém a forma serializada de tudo o que está abaixo dele. Isso também significa que nenhum tipo aparece duas vezes, o que limita `count` ao número de tipos.

Observe que essa ordem não corresponde à ordem dos valores dos bytes de tipo acima. Qualquer outra pilha descreve um layout de coluna que nenhum gravador constrói e que nenhuma etapa de materialização consegue desencapsular de volta em uma coluna completa do tipo declarado — por exemplo, um `ColumnSparse` cuja coluna de valores seja, ela própria, esparsa ou replicada permanece encapsulado.

**`kind_stack` recursivo para colunas `Tuple`.** A carga útil de `kind_stack` descrita acima é o byte (ou a sequência `COMBINATION`) das informações de serialização de uma única coluna. Um `Tuple` carrega um `SerializationInfoTuple`, que primeiro grava a carga útil de pilha de tipos *da própria* tupla e depois grava uma carga útil completa de pilha de tipos para *cada* elemento, na ordem; um decodificador lê de volta essa mesma estrutura recursiva. Assim, para `Tuple(A, B, C)`, os bytes do campo 4 são `[tuple_kind][A_kind][B_kind][C_kind]`, e a carga útil de cada elemento também é recursiva se esse elemento for, por sua vez, composto. O byte `has_custom_serialization` (campo 3) é definido sempre que as informações da própria tupla *ou de qualquer elemento* forem diferentes do padrão; portanto, um `Tuple` cujo único elemento especial seja esparso ou replicado também aciona a carga útil de pilha de tipos. Um decodificador que leia apenas o byte de enum inicial de um `Tuple` vai parar cedo demais e interpretar os bytes de tipo restantes dos elementos, por engano, como dados de coluna.

**Formato wire esparso.** Quando `kind_stack = 0x01`, o `data` da coluna consiste em dois fluxos gravados um após o outro no único stream TCP compartilhado:

1. **Fluxo de offsets** — uma sequência de `VarUInt`s. Cada valor `v` pode ser:
   * `v` com o bit alto na posição 62 desligado: `(v & 0x3FFFFFFFFFFFFFFF)` = o número de posições com valor default antes do próximo valor explícito diferente do padrão. Essa posição diferente do padrão é `cursor + group_size`, em que `cursor` é a posição corrente; em seguida, `cursor` avança `group_size + 1`.
   * `v` com o bit 62 ligado (`END_OF_GRANULE_FLAG`): o valor com o flag desligado = o número de posições finais com valor default após o último valor diferente do padrão. Isso marca o fim do fluxo de offsets do bloco.
2. **Stream de valores** — `count` valores diferentes do padrão, codificados de forma densa no tipo interno, em que `count` é o número de VarUInts não EOG lidos acima.

Um decodificador reconstrói uma coluna densa de `num_rows` entradas preenchendo cada posição não explícita com o valor default do tipo interno (`0` para inteiros e floats, `""` para `String`, `0` dias para `Date` e assim por diante).

Uma coluna `Nullable(T)` esparsa é um caso especial, pois o valor default de `Nullable(T)` é **NULL**. A codificação esparsa elimina por completo o stream de mapa de nulos que `Nullable` normalmente usa: o fluxo de offsets identifica as posições diferentes do padrão — isto é, não NULL —, o stream de valores contém apenas esses valores não NULL, de forma densa em `T`, e toda posição não explícita é reconstruída como NULL. Portanto, um decodificador *não* deve procurar um mapa de nulos no stream de valores nem preencher as lacunas com um `0` presente; ele deve preenchê-las com NULL.

**Formato wire Replicated.** Quando `kind_stack = 0x04`, o `data` da coluna é um dicionário: uma lista de valores distintos de elementos, acompanhada de um índice por linha que aponta para essa lista (a mesma estrutura de lookup de `LowCardinality`). Quando o próprio tipo interno é versionado — por exemplo, `LowCardinality(T)` —, seu prefixo de estado é gravado **primeiro**, antes do stream de índices: a serialização replicada delega a fase de prefixo ao tipo interno antes de gravar `num_rows`. Tipos internos com prefixo vazio (os tipos folha e os compostos simples) não acrescentam nenhum byte aqui.

```text theme={null}
[inner type's state prefix]              empty for leaf inners; e.g. LowCardinality version (Int64 = 1)
[VarUInt num_rows]
[UInt8  size_of_indexes_type]            width of each index: 1, 2, 4, or 8 bytes
[indexes: num_rows × size_of_indexes_type bytes]
[VarUInt num_elements]
[elements: num_elements dense inner-type values]
```

Um decodificador reconstrói uma coluna densa selecionando `elements[indexes[i]]` para cada linha de saída `i`. Tipos internos compostos são processados recursivamente: a lista de elementos é materializada no tipo interno e, em seguida, indexada. Os tipos internos compatíveis incluem os tipos folha, `Nullable(T)`, `Array(T)`, `Tuple(...)`, `Map(K, V)`, `Nested(...)` (cada campo é expandido como um `Array`) e `LowCardinality(T)` (o dicionário compartilhado é mantido; apenas as chaves de cada elemento são indexadas).

**Formato wire desanexado.** `DETACHED` (`0x02`) e `DETACHED_OVER_SPARSE` (`0x03`) *de fato* aparecem na transmissão pela rede — não são puramente internos. No caminho TCP, quando a compressão está habilitada e a revisão negociada é no mínimo `DBMS_MIN_REVISON_WITH_PARALLEL_BLOCK_MARSHALLING` (v54478), a coluna passa por três etapas:

1. Cada coluna elegível (não `const`, não `Tuple`, em um bloco com mais de uma linha) é encapsulada em um `ColumnBLOB`, que contém a coluna já serializada e comprimida fora da thread principal.
2. `DETACHED` é anexado à pilha de tipos da coluna encapsulada.
3. O `data` da coluna é gravado como o tamanho do blob em `VarUInt`, seguido de exatamente essa quantidade de bytes do blob.

Se a coluna encapsulada for esparsa, sua pilha será `{DEFAULT, SPARSE, DETACHED}`, serializada como `DETACHED_OVER_SPARSE`. Um cliente que decodifica uma coluna desse tipo lê o comprimento e os bytes do blob e, em seguida, descomprime o blob para recuperar a carga útil da coluna interna (consulte a [nota sobre `ColumnBLOB`](#compression-negotiation) na seção de compressão).

`DETACHED` é direcional: só pode aparecer em blocos de resultado enviados por um servidor e apenas para uma coluna inteira de nível superior — nunca como um dos tipos de elemento de uma `Tuple`. Ao contrário dos outros tipos, ele faz com que a coluna em tempo de execução seja uma `ColumnBLOB`, e não a coluna do tipo declarado; por isso, um receptor só o aceita quando seu próprio pipeline converte o blob de volta. Assim, um servidor rejeita `DETACHED` com `INCORRECT_DATA` em qualquer bloco enviado por um cliente, seja ele de dados de tabela externa, de escalares ou de dados de insert.

### Variantes de bloco

Todos os pacotes da família Data compartilham o mesmo formato wire de bloco. As variantes diferem apenas na quantidade de colunas e de linhas:

| Variante | num\_columns | num\_rows | Finalidade |
| - | - | - | - |
| Bloco de cabeçalho | N > 0 | 0 | Anuncia o schema do resultado (nomes e tipos das colunas). |
| Bloco de resultado | N > 0 | M > 0 | Linhas de resultado propriamente ditas. |
| Bloco vazio | 0 | 0 | Sentinela — fim da entrada no lado do cliente; marcador de fronteira no lado do servidor. |

### Exemplos em nível de byte

Todos os exemplos desta seção foram extraídos do **caminho do Data-packet via TCP**, portanto incluem o prefixo `BlockInfo` e o byte `has_custom_serialization`. Via `FORMAT Native`, os mesmos blocos são mais curtos — a forma curta equivalente é apresentada quando for útil.

Um bloco vazio (com BlockInfo), com 8 bytes no total:

```text theme={null}
01 00                   BlockInfo: field_id=1, is_overflows=0
02 FF FF FF FF          BlockInfo: field_id=2, bucket_number=-1
00                      BlockInfo terminator
00                      num_columns = 0
00                      num_rows = 0
```

Um bloco de cabeçalho para `SELECT 1` declara uma coluna chamada `"1"` do tipo `UInt8`, com zero linhas. A partir da versão de protocolo 54454, o byte `has_custom_serialization` é incluído:

```text theme={null}
01 00                   BlockInfo: is_overflows = 0
02 FF FF FF FF          BlockInfo: bucket_number = -1
00                      BlockInfo terminator
01                      num_columns = 1
00                      num_rows = 0
01 "1"                  Column[0].name = "1"
05 "UInt8"              Column[0].type = "UInt8"
00                      Column[0].has_custom_serialization = 0
                        Column[0].data: no bytes (num_rows = 0)
```

O bloco de resultado da mesma consulta, com uma linha:

```text theme={null}
01 00                   BlockInfo: is_overflows = 0
02 FF FF FF FF          BlockInfo: bucket_number = -1
00                      BlockInfo terminator
01                      num_columns = 1
01                      num_rows = 1
01 "1"                  Column[0].name = "1"
05 "UInt8"              Column[0].type = "UInt8"
00                      Column[0].has_custom_serialization = 0
01                      Column[0].data: one UInt8 byte = 1
```

Com `FORMAT Native` (revisão `0`), o mesmo bloco de resultado não tem `BlockInfo` nem o byte `has_custom_serialization` — `SELECT 1 FORMAT Native` ocupa 11 bytes:

```text theme={null}
01                      num_columns = 1
01                      num_rows = 1
01 "1"                  Column[0].name = "1"
05 "UInt8"              Column[0].type = "UInt8"
01                      Column[0].data: one UInt8 byte = 1
```

(Um resultado com zero linhas, como um bloco contendo apenas o cabeçalho, não gera nenhum byte com `FORMAT Native`: esse formato de saída não emite blocos vazios.)

## Revisão do protocolo e o formato Native

Um fluxo de bytes Native é definido, acima de tudo, pela **revisão do protocolo** usada pelo componente de gravação e pelo leitor. A revisão não aparece em lugar nenhum nos próprios bytes — não existe campo de revisão no wire —, mas ainda assim determina se vários recursos aparecem ou não. Por isso, um decodificador precisa saber em qual revisão uma carga útil foi gravada antes de conseguir interpretá-la. Como a revisão não está no stream, o leitor e o componente de gravação precisam chegar a um acordo sobre ela de alguma outra forma.

Trata-se de um único `UInt64`, que tanto `NativeWriter` quanto `NativeReader` recebem como argumento do construtor. No componente de gravação, ele se chama `client_revision` e, no leitor, `server_revision`, mas é o mesmo número. A revisão mais recente conhecida por este lançamento é `DBMS_TCP_PROTOCOL_VERSION` (consulte `src/Core/ProtocolDefines.h`).

### O que a revisão controla

Cada recurso depende de um limiar `DBMS_MIN_REVISION_WITH_*`. O componente de gravação só emite o recurso quando a revisão atinge o limiar, e o leitor o procura seguindo exatamente a mesma regra, de modo que ambos permanecem sincronizados — se a revisão estiver errada em qualquer um dos lados, eles perdem a sincronia. Os controles relevantes para o formato Native são:

| Recurso | Constante de limiar | Revisão | Efeito abaixo do limiar |
| - | - | - | - |
| Prefixo `BlockInfo` | (qualquer valor `> 0`) | `1` | O prefixo [`BlockInfo`](#blockinfo) é omitido por completo; um bloco consiste apenas em `num_columns`, `num_rows` e as colunas. |
| Byte `has_custom_serialization` | `DBMS_MIN_REVISION_WITH_CUSTOM_SERIALIZATION` | `54454` | O byte [`has_custom_serialization`](#column-wire-layout) por coluna é omitido; todas as colunas usam a serialização padrão (sem as formas esparsa, replicada ou desanexada). |
| `LowCardinality` no wire | `DBMS_MIN_REVISION_WITH_LOW_CARDINALITY_TYPE` | `54405` | Caso especial — **não** segue a regra simples de comportamento abaixo do limiar. `LowCardinality(T)` é reduzido ao tipo básico `T` somente quando a revisão é *diferente de zero* e inferior a `54405`, ou quando a redução é forçada separadamente. Com a revisão `0`, o tipo é mantido. Veja a nota abaixo. |
| Serialização V2 de `Dynamic` / `JSON` | `DBMS_MIN_REVISION_WITH_V2_DYNAMIC_AND_JSON_SERIALIZATION` | `54473` | `Dynamic` e `JSON`/`Object` usam a serialização V1 (com o parâmetro `max_dynamic_*`) em vez da V2. |
| Serialização de `String` com offsets | `DBMS_MIN_REVISION_WITH_STRING_WITH_SIZE_STREAM_SERIALIZATION` | `54492` | Abaixo do limiar, os dados de colunas `String` usam o layout por valor (prefixo de comprimento `VarUInt` + bytes brutos por linha) em vez do [layout de offsets](#string-type) (offsets cumulativos de bytes como `UInt64`, seguidos de todos os dados concatenados). O controle depende exclusivamente da revisão, sem marcador no wire por coluna. No limiar ou acima dele, o layout de offsets também se aplica a `String` aninhada em tipos compostos (`Array`, `Nullable`, `Map`, `Tuple`, `Variant`, `Dynamic`, `JSON`), mas **não** ao dicionário de uma coluna `LowCardinality`: um dicionário `LowCardinality` é sempre gravado com a serialização aninhada padrão, de modo que seus valores `String` mantêm o layout por valor (isso inclui `LowCardinality(String)` e `LowCardinality(Nullable(String))`). O formato `Buffers` não faz parte do protocolo e sempre usa o layout por valor. |
| Versionamento de funções de agregação | `DBMS_MIN_REVISION_WITH_AGGREGATE_FUNCTIONS_VERSIONING` | `54452` | O estado de `AggregateFunction` é gravado sem versão embutida. |
| `skip_degree` no estado de `quantileDeterministic` | `DBMS_MIN_REVISION_WITH_QUANTILE_DETERMINISTIC_SKIP_DEGREE` | `54491` | Os estados de `AggregateFunction(quantileDeterministic, ...)` (e as variantes `quantiles`/`median` da mesma função) são gravados na versão de estado `0`, sem o `UInt8 skip_degree` final. Veja a nota em [aliases de tipo](#type-aliases). |
| Amostra de hashes de 64 bits no estado de `uniq` | `DBMS_MIN_REVISION_WITH_UNIQ_STATE_VERSION_1` | `54493` | Os estados de `AggregateFunction(uniq, ...)` são gravados na versão de estado `0`: sem o byte de flags inicial e sem a amostra final de hashes de 64 bits. Veja a nota em [aliases de tipo](#type-aliases). |
| `out_of_order_buckets` em `BlockInfo` | `DBMS_MIN_REVISION_WITH_OUT_OF_ORDER_BUCKETS_IN_AGGREGATION` | `54480` | O campo de ID `3` de `BlockInfo` não é gravado (veja [BlockInfo](#blockinfo)). |
| Marshalling paralelo de bloco (`DETACHED`) | `DBMS_MIN_REVISON_WITH_PARALLEL_BLOCK_MARSHALLING` | `54478` | As colunas nunca são encapsuladas em um `ColumnBLOB`; os tipos `DETACHED` / `DETACHED_OVER_SPARSE` nunca aparecem (veja [kind\_stack](#kind-stack-and-sparse-encoding)). |
| Parâmetro de tipo `DateTime(tz)` | `DBMS_MIN_REVISION_WITH_TIME_ZONE_PARAMETER_IN_DATETIME_DATA_TYPE` | `54337` | O parâmetro de fuso horário é removido da string `type` — `DateTime('UTC')` é anunciado apenas como `DateTime`. |

Isso torna a revisão `0` a codificação mais conservadora para quase tudo: o stream não carrega `BlockInfo` nem o byte `has_custom_serialization`, usa `Dynamic`/`JSON` V1, não tem versão de função de agregação e traz um `DateTime` simples, sem o parâmetro de fuso horário. As versões de *estado* de funções de agregação são a exceção na revisão `0`: não há nenhum par a partir do qual derivar uma versão — o stream é gravado e lido de volta por quem o produziu (um arquivo de dados `StripeLog`, um backup de `Set`/`Join`, um arquivo no formato `Native`) —, então o escritor mantém a versão fixada no tipo (explicitando-a na string do tipo, como de costume) em vez de rebaixar o estado para a versão `0`, e somente um tipo que não fixa nenhuma versão é gravado na versão `0`.

`LowCardinality` é a única exceção, e uma exceção importante. A verificação do escritor é `remove_low_cardinality || (client_revision && client_revision < DBMS_MIN_REVISION_WITH_LOW_CARDINALITY_TYPE)`. O truque está no `client_revision &&` inicial: quando a revisão é exatamente `0`, a condição inteira entra em curto-circuito e resulta em falso.

Portanto, na revisão `0` — o padrão para `FORMAT Native` —, `LowCardinality(T)` **não** é removido. Sua string do tipo e o prefixo de estado por bloco permanecem no stream, e o leitor da revisão `0` os lê de volta diretamente. A remoção só ocorre em uma revisão diferente de zero inferior a `54405` ou quando é forçada, independentemente da revisão.

Quem força a remoção é a flag `remove_low_cardinality`. A saída de `FORMAT Native` nunca a define, mas o caminho TCP nativo, sim, quando `low_cardinality_allow_in_native_format = 0` (padrão `1`). Em outras palavras, essa configuração altera a saída TCP nativa, mas não tem efeito algum sobre `FORMAT Native`.

Na prática: um stream `FORMAT Native` padrão pode legitimamente conter `LowCardinality`, portanto não o trate como um recurso ausente na revisão `0`.

### De onde vem a revisão, dependendo de como os dados trafegam

Os mesmos bytes Native podem trafegar por caminhos diferentes: o protocolo TCP nativo, uma requisição HTTP ou um arquivo em disco. Cada caminho define a revisão à sua maneira. Um ponto de atenção: o lado de leitura e o lado de escrita são configurados separadamente, então podem acabar usando revisões diferentes.

#### Protocolo TCP nativo — negociado, em ambas as direções

No [protocolo TCP nativo](https://clickhouse.com/docs/pt-BR/resources/develop-contribute/native-protocol/protocol.md), a revisão é definida pelo handshake Hello. O cliente envia `DBMS_TCP_PROTOCOL_VERSION`, o servidor responde com a sua própria e, a partir daí, cada lado serializa na **revisão anunciada pelo outro lado**: o servidor cria seu `NativeReader`/`NativeWriter` a partir de `client_tcp_protocol_version`, enquanto o cliente usa o `server_revision` que recebeu. Não há um `min` explícito, mas nenhum dos lados consegue emitir um recurso que não implementou; por isso, cada direção fica, na prática, limitada pelo mais antigo dos dois pares.

Quando ambos os pares usam a mesma compilação recente, as duas direções chegam à mesma revisão (`DBMS_TCP_PROTOCOL_VERSION`, consulte `src/Core/ProtocolDefines.h`) e todas as condições de controle ficam ativas. Esse é o caso mais comum, mas não é garantido. Com pares de versões diferentes ou de terceiros, as duas direções podem ficar em revisões distintas, então as condições de controle precisam ser avaliadas por direção: `BlockInfo` está presente em qualquer revisão diferente de zero, mas o restante — incluindo `has_custom_serialization` — só aparece quando a revisão efetiva daquela direção atinge os respectivos limiares. Um par que anuncia uma revisão inferior a `54454`, por exemplo, não envia nem recebe o byte `has_custom_serialization`.

#### Saída `FORMAT Native` — revisão 0 por padrão, elevável via HTTP

O formato de *saída* `Native` usa por padrão a revisão **`0`**. Isso abrange `SELECT ... FORMAT Native` via HTTP, `INTO OUTFILE ... FORMAT Native` e a saída `Native` gravada pelo `clickhouse-client`; em todos esses casos, a fábrica de saída repassa `FormatSettings::client_protocol_version` diretamente ao `NativeWriter`, que controla o enquadramento de `BlockInfo` / `has_custom_serialization` *e* o layout de offsets de `String`. Assim, um `SELECT ... FORMAT Native` via HTTP com um `client_protocol_version` elevado (≥ `54492`) grava o layout de offsets, enquanto a revisão padrão `0` mantém o layout portável por valor.

Via HTTP, porém, esse padrão não é a história completa. Um cliente pode elevá-lo com o parâmetro de consulta `?client_protocol_version=<n>`, que o HTTP handler trata como um parâmetro reservado, e não como uma configuração SQL: ele vai para o contexto da consulta, e a camada de formato o copia para `FormatSettings`. Com um valor alto o suficiente, a saída `FORMAT Native` via HTTP passa a incluir o prefixo `BlockInfo` e o byte `has_custom_serialization`, assim como no caminho TCP — portanto, não presuma que uma carga útil `FORMAT Native` via HTTP esteja sempre na revisão `0`.

Como o parâmetro é aplicado a todo o contexto da consulta, ele chega a todos os `NativeWriter` criados para aquela requisição, e não apenas ao stream de resultado. Em particular, uma gravação do lado do servidor como `INSERT INTO FUNCTION file('x.native', 'Native', ...) SELECT ...` via HTTP com um `?client_protocol_version=` elevado grava o arquivo nessa revisão. Esse comportamento é antigo (é anterior à serialização de offsets de String e também se aplica a `BlockInfo` e `has_custom_serialization`) e assimétrico: o lado de leitura (veja abaixo) não é elevado da mesma forma, de modo que esse arquivo não pode ser lido de volta por meio de `file(...)`. Não inclua `client_protocol_version` em requisições que gravem dados que precisarão ser lidos novamente. `INTO OUTFILE` e a saída local do `clickhouse-client` não oferecem esse ajuste e permanecem em `0`.

#### Entrada `FORMAT Native` — sempre revisão 0

O formato de *entrada* `Native` sempre cria seu `NativeReader` na revisão **`0`**. Ele nunca espera um prefixo `BlockInfo`, nunca lê o byte `has_custom_serialization` e sempre pressupõe a serialização padrão — seja ao fazer o parsing do corpo de um `INSERT ... FORMAT Native`, ao ler um arquivo `Native` ou ao inferir um schema. `?client_protocol_version=<n>` não eleva a revisão de entrada (apenas o lado de saída lê esse parâmetro).

Como a revisão de entrada é sempre `0`, o formato de entrada sempre lê o layout de `String` por valor; um stream gravado com o layout de offsets (pelo protocolo TCP nativo ou por meio de um `SELECT ... FORMAT Native` com `client_protocol_version` elevado) não pode ser lido de volta pela entrada `FORMAT Native` — a mesma assimetria que ocorre com o enquadramento `BlockInfo`.

### Implicações de ida e volta

Para `FORMAT Native`, o padrão é a revisão `0` em ambas as pontas. Os dados gravados por `SELECT ... FORMAT Native` na revisão `0` podem ser lidos de volta diretamente por `INSERT ... FORMAT Native`, sem surpresas.

Um stream produzido com uma revisão de saída elevada (`?client_protocol_version=<n>` no `SELECT`) carrega o enquadramento `BlockInfo` e `has_custom_serialization`, além do layout de `String` com offsets, e nada disso é lido de volta pelo formato de entrada, que está sempre na revisão `0`. Portanto, elevar `client_protocol_version` só faz sentido para um consumidor que fala o protocolo TCP nativo, e não para uma ida e volta com `FORMAT Native` via HTTP.

Arquivos são o caso assimétrico. Um `?client_protocol_version=` elevado em um `INSERT INTO FUNCTION file(...)` (ou `s3`, `url`) no lado do servidor *grava* o arquivo nessa revisão, mas uma *leitura* com `file(...)` sempre analisa na revisão `0`; por isso, esse arquivo não faz ida e volta. Deixe `client_protocol_version` fora da requisição de gravação, para que o arquivo fique simplesmente na revisão `0`, ou transfira os dados pelo protocolo TCP nativo, no qual cada direção usa a revisão negociada no handshake.

| Canal | Revisão de gravação | Revisão de leitura | `BlockInfo` / serialização personalizada |
| - | - | - | - |
| Pacote Data do TCP nativo | Revisão anunciada pelo par (por direção) | Revisão anunciada pelo par (por direção) | `BlockInfo` sempre que a revisão for `> 0`; `has_custom_serialization` a partir de `≥ 54454` |
| `SELECT ... FORMAT Native` via HTTP | `client_protocol_version` (padrão `0`) | n/a | Somente se `client_protocol_version` for elevado |
| `INSERT ... FORMAT Native` via HTTP (corpo) | n/a | `0` (sempre) | Sem enquadramento; `String` sempre por valor |
| `INSERT INTO FUNCTION file`/`s3`/`url(..., 'Native')` via HTTP | `client_protocol_version` (padrão `0`) | `0` (leituras sempre) | Na gravação, somente se `client_protocol_version` for elevado — esse arquivo não pode ser lido de volta |
| `INTO OUTFILE` / `clickhouse-client` local `FORMAT Native` | `0` | `0` | Ausente (mas `LowCardinality` é mantido — veja a nota acima) |

> [!NOTE]
>
> **Revisão do protocolo vs. versão de serialização**
>
> Não confunda a revisão do protocolo com a [versão de serialização](#serialization-version-concept). A revisão, aqui, vale para toda a conexão ou requisição e nunca aparece nos bytes. A versão de serialização é definida por coluna, carregada pelos [tipos versionados](#versioned-types), e é gravada em todo bloco não vazio. A revisão determina se um recurso existe ou não; já a versão de serialização, uma vez dentro de uma coluna versionada, define qual variante da codificação daquele tipo específico vem em seguida.

## Tipos de dados

Esta seção documenta a codificação no wire dos tipos que o formato Native pode transportar no `data` de uma coluna, agrupados em quatro famílias em ordem crescente de complexidade para o decodificador. Dois tipos — `AggregateFunction(func, ...)` e `QBit(T, N[, stride])` — são tipos de coluna `Native` válidos, mas têm cargas úteis específicas da função ou do tipo, que estão fora do escopo deste documento; eles são sinalizados abaixo nos pontos em que poderiam ser confundidos com aliases.

| Família | Seção | Streams por coluna | Estado entre blocos |
| - | - | - | - |
| Largura fixa | [Tipos de largura fixa](#fixed-width-types) | Um | Nenhum |
| Comprimento variável | [Tipos de comprimento variável](#variable-length-types) | Um | Nenhum |
| Composto (formato fixo) | [Tipos compostos](#composite-types) | Vários | Nenhum |
| Versionado / com estado | [Tipos versionados](#versioned-types) | Vários | Nenhum no wire Native — prefixo de estado por bloco, reiniciado a cada bloco |

### Tipos de largura fixa

Cada valor ocupa um número constante de bytes. Uma coluna de `M` linhas ocupa exatamente `bytes_per_row × M` bytes no wire, concatenados sem separadores nem preenchimento.

| String do tipo | Bytes por valor | Valor lógico | Codificação no wire |
| - | - | - | - |
| `UInt8` | 1 | Inteiro sem sinal de 8 bits | Byte bruto |
| `UInt16` | 2 | Inteiro sem sinal de 16 bits | Little-endian |
| `UInt32` | 4 | Inteiro sem sinal de 32 bits | Little-endian |
| `UInt64` | 8 | Inteiro sem sinal de 64 bits | Little-endian |
| `UInt128` | 16 | Inteiro sem sinal de 128 bits | Little-endian |
| `UInt256` | 32 | Inteiro sem sinal de 256 bits | Little-endian |
| `Int8` | 1 | Inteiro com sinal de 8 bits, complemento de dois | Byte bruto |
| `Int16` | 2 | Inteiro com sinal de 16 bits, complemento de dois | Little-endian |
| `Int32` | 4 | Inteiro com sinal de 32 bits, complemento de dois | Little-endian |
| `Int64` | 8 | Inteiro com sinal de 64 bits, complemento de dois | Little-endian |
| `Int128` | 16 | Inteiro com sinal de 128 bits, complemento de dois | Little-endian |
| `Int256` | 32 | Inteiro com sinal de 256 bits, complemento de dois | Little-endian |
| `Float32` | 4 | IEEE 754 de precisão simples | Little-endian |
| `Float64` | 8 | IEEE 754 de precisão dupla | Little-endian |
| `BFloat16` | 2 | 16 bits superiores de um `Float32` IEEE 754 | Little-endian |
| `Bool` | 1 | `0x00` = false, `0x01` = true | Byte bruto |
| `Date` | 2 | Dias desde `1970-01-01` | UInt16 little-endian |
| `Date32` | 4 | Dias desde `1970-01-01` (com sinal; aceita datas anteriores a 1970) | Int32 little-endian |
| `DateTime` | 4 | Unix timestamp em segundos | UInt32 little-endian |
| `DateTime(tz)` | 4 | Igual a `DateTime`; o fuso horário é metadado | UInt32 little-endian |
| `DateTime64(s)` | 8 | Ticks no escalar `s` (10^-s segundos desde o epoch) | Int64 little-endian |
| `DateTime64(s, tz)` | 8 | Igual a `DateTime64(s)`; o fuso horário é metadado | Int64 little-endian |
| `Time` | 4 | Duração de relógio com sinal, em segundos | Int32 little-endian |
| `Time64(s)` | 8 | Duração de relógio com sinal, em ticks no escalar `s` | Int64 little-endian |
| `Interval<Unit>` | 8 | Contagem com sinal; a unidade fica na string do tipo | Int64 little-endian |
| `UUID` | 16 | Identificador de 128 bits | Duas metades UInt64 LE com bytes invertidos (veja [UUID](#uuid)) |
| `IPv4` | 4 | Endereço IPv4 | UInt32 little-endian |
| `IPv6` | 16 | Endereço IPv6 | Ordem de bytes de rede, sem inversão |
| `Enum8` | 1 | Inteiro com sinal de 8 bits (índice da variante) | Byte bruto |
| `Enum16` | 2 | Inteiro com sinal de 16 bits (índice da variante) | Little-endian |
| `Decimal(P, S)` | 4 / 8 / 16 / 32 | `value × 10^S` como inteiro com sinal; a largura depende de P (≤9 → 4 B, ≤18 → 8 B, ≤38 → 16 B, ≤76 → 32 B) | Inteiro com sinal little-endian |

#### Tipos inteiros

`UInt8`–`UInt256` e `Int8`–`Int256` são codificações binárias diretas de valores inteiros. Um decodificador lê `bytes_per_row × num_rows` bytes e os interpreta de acordo com o tipo.

Uma coluna `UInt32` com os valores `[1, 256, 65536]`:

```text theme={null}
01 00 00 00              row 0: 1
00 01 00 00              row 1: 256
00 00 01 00              row 2: 65536
```

Uma coluna `Int32` contendo `[-1, 42]`:

```text theme={null}
FF FF FF FF              row 0: -1
2A 00 00 00              row 1: 42
```

#### Float32 e Float64

Floats binários no padrão IEEE 754: 4 bytes em precisão simples (`binary32`) e 8 bytes em precisão dupla (`binary64`), ambos em little-endian. NaN, ±Infinity, ±0.0 e valores subnormais são preservados na ida e volta sem normalização.

Valor `Float32` `1.5` (`0x3FC00000`):

```text theme={null}
00 00 C0 3F              little-endian IEEE 754
```

Valor `Float64` `1.5` (`0x3FF8000000000000`):

```text theme={null}
00 00 00 00 00 00 F8 3F  little-endian IEEE 754
```

#### BFloat16

O formato brain-floating-point: os 16 bits mais significativos de um `Float32` IEEE 754 — 1 bit de sinal, 8 bits de expoente e 7 bits de mantissa. Cada valor ocupa 2 bytes, em little-endian, e armazena o padrão bruto de 16 bits. Para recuperar o valor numérico, expanda-o de volta para `Float32`, colocando o padrão na metade superior e zerando a metade inferior (`bits << 16` reinterpretado como `Float32`); o valor expandido passa então a usar a mesma formatação de texto do `Float32`.

Valor `BFloat16` `1.5` (padrão `0x3FC0`, a metade superior do `Float32` `0x3FC00000`):

```text theme={null}
C0 3F                    little-endian, widens to Float32 1.5
```

#### Bool

Compatível no wire com `UInt8`: 1 byte por linha, `0x00` = false, `0x01` = true. A string do tipo no wire é literalmente `Bool` (e não `UInt8`); portanto, um decodificador que faz o encaminhamento com base na string do tipo precisa reconhecê-la separadamente.

Uma coluna `Bool` `[true, false, true]`:

```text theme={null}
01 00 01
```

#### Date e Date32

Ambos codificam datas como contagens inteiras de dias a partir da Unix epoch `1970-01-01`. Nenhum dos dois inclui um componente de hora.

| Tipo | Bytes | Codificação | Intervalo |
| - | - | - | - |
| `Date` | 2 | UInt16 little-endian | `1970-01-01` a `2149-06-06` |
| `Date32` | 4 | Int32 little-endian | amplo intervalo com sinal, aceita datas anteriores a 1970 |

Valor `Date` `1970-01-02` (1 dia):

```text theme={null}
01 00                    UInt16 LE = 1
```

Valor `Date32` `1900-01-01` (-25567 dias):

```text theme={null}
21 9C FF FF              Int32 LE = -25567
```

#### DateTime

Compatível no wire com `UInt32`: um timestamp Unix em segundos, codificado em 4 bytes little-endian. O tipo pode aparecer como `DateTime` ou `DateTime('Timezone')`; o fuso horário afeta apenas a exibição e não faz parte do valor no wire. Duas colunas `DateTime` com parâmetros de fuso horário diferentes geram bytes idênticos para o mesmo instante. Um decodificador remove o sufixo de parâmetro `(...)` e processa a coluna como `UInt32`.

Valor `DateTime('UTC')` `2024-03-15 14:30:00 UTC` (timestamp `1710513000`):

```text theme={null}
68 5B F4 65              UInt32 LE = 1710513000
```

#### DateTime64(escalar\[, timezone])

8 bytes, Int64 little-endian representando ticks no escalar de `10^-scale` segundos desde a Unix epoch. O parâmetro `scale` (0–9) fica na string do tipo e define a unidade de tempo:

| Escalar | Tamanho do tick | Nome comum |
| - | - | - |
| 0 | 1 segundo | segundos |
| 3 | 1 milissegundo | ms |
| 6 | 1 microssegundo | µs |
| 9 | 1 nanossegundo | ns |

O tipo aparece como `DateTime64(s)` (fuso horário padrão do servidor, implícito) ou `DateTime64(s, 'TimezoneName')` (fuso horário explícito, usado apenas na exibição). Valores negativos representam ticks anteriores à epoch.

Valor `DateTime64(3, 'UTC')` `2024-01-15 12:30:45.123 UTC` (1705321845123 ms):

```text theme={null}
83 51 1A 0D 8D 01 00 00  Int64 LE = 1705321845123
```

Valor `DateTime64(0)` `2024-01-15 12:30:45 UTC` (1705321845 s):

```text theme={null}
75 25 A5 65 00 00 00 00  Int64 LE = 1705321845
```

#### Time e Time64(scale)

Uma duração de relógio, e não um ponto no tempo. `Time` é uma contagem de segundos com sinal, em 4 bytes Int32 little-endian; `Time64(scale)` é uma contagem de ticks com sinal no escalar decimal informado (0–9), em 8 bytes Int64 little-endian — o mesmo formato wire de `DateTime64`.

A forma textual é `[-]HH:MM:SS[.fraction]`, mas, diferentemente de `DateTime`, o campo de hora **não** é reiniciado a cada dia de 24 horas: ele representa a contagem total de horas e pode passar de 23. A magnitude exibida é limitada a `999:59:59` (`3599999` segundos); uma magnitude maior é exibida com os segundos no limite e a fração armazenada mantida como está, de modo que uma contagem de ticks de `3700000.25` é exibida como `999:59:59.250000`. `CAST` satura o valor armazenado no maior tick representável no escalar de destino (`999:59:59.999999` para `Time64(6)`), e não no segundo inteiro, enquanto operações aritméticas ainda podem deixar na coluna uma contagem de ticks fora do intervalo, que só é limitada na exibição. Nada disso afeta os bytes wire, que são simplesmente o inteiro com sinal.

Valor `Time` `45296` (`12:34:56`):

```text theme={null}
F0 B0 00 00              Int32 LE = 45296
```

Valor `Time64(3)` de `45296789` ticks (`12:34:56.789`):

```text theme={null}
95 2C B3 02 00 00 00 00  Int64 LE = 45296789
```

> [!NOTE]
>
> `Time` e `Time64` são experimentais e exigem `allow_experimental_time_time64_type = 1` no servidor.

#### Interval

`Interval<Unit>` — `IntervalSecond`, `IntervalMinute`, `IntervalHour`, `IntervalDay`, `IntervalWeek`, `IntervalMonth`, `IntervalQuarter`, `IntervalYear`, `IntervalNanosecond` etc. Todas as unidades compartilham a mesma codificação wire: a contagem como um Int64 com sinal de 8 bytes em little-endian. A unidade existe **apenas** na string do tipo — ela não altera os bytes wire nem a forma textual, que é simplesmente o inteiro. Um único caminho de decodificação trata todas as unidades.

`IntervalDay` com valor `5`:

```text theme={null}
05 00 00 00 00 00 00 00  Int64 LE = 5
```

#### UUID

16 bytes por valor. A codificação de wire **não** corresponde aos 16 bytes canônicos em big-endian — cada metade de 8 bytes tem seus bytes invertidos de forma independente.

O modelo lógico é um identificador de 128 bits na forma textual canônica `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx`, em que os bytes são, por convenção, escritos em big-endian. O modelo de wire toma esses 16 bytes canônicos, divide-os em duas metades de 8 bytes e grava cada metade em little-endian:

* Bytes de wire 0..7 = bytes canônicos 0..7 em ordem inversa.
* Bytes de wire 8..15 = bytes canônicos 8..15 em ordem inversa.

UUID `550e8400-e29b-41d4-a716-446655440000`:

```text theme={null}
Canonical bytes (16):    55 0E 84 00 E2 9B 41 D4  A7 16 44 66 55 44 00 00

Wire bytes:
D4 41 9B E2 00 84 0E 55  high half byte-reversed
00 00 44 55 66 44 16 A7  low half byte-reversed
```

O UUID nulo (composto apenas por zeros) é idêntico em ambas as representações.

#### IPv4 e IPv6

Dois tipos de endereço relacionados, mas codificados de formas diferentes.

`IPv4` ocupa 4 bytes e é codificado como um UInt32 little-endian que armazena o endereço canônico de 32 bits (o valor `(a << 24) | (b << 16) | (c << 8) | d` obtido de `a.b.c.d`). Os bytes no wire correspondem aos bytes em ordem de rede invertidos.

`192.168.1.10` (valor canônico de 32 bits `0xC0A8010A`):

```text theme={null}
0A 01 A8 C0              Little-endian UInt32
```

`IPv6` ocupa 16 bytes, gravados **tal como estão, na ordem de bytes de rede (network byte order)**, sem inversão — a mesma ordem de bytes de `inet_pton(AF_INET6, ...)`.

`2001:db8::1`:

```text theme={null}
20 01 0D B8 00 00 00 00  network bytes 0..7
00 00 00 00 00 00 00 01  network bytes 8..15
```

A assimetria é intencional: o IPv4 é armazenado como `u32` para permitir operações aritméticas e consultas de intervalo compactas, enquanto o IPv6 mantém o layout em ordem de bytes de rede, comum à maioria das APIs de rede.

#### Enum8 e Enum16

Compatíveis no wire com `Int8` e `Int16`, respectivamente: 1 ou 2 bytes por linha, em complemento de dois little-endian no caso da variante de 16 bits. O mapeamento completo das variantes fica na string do tipo:

```text theme={null}
Enum8('active' = 1, 'inactive' = 2, 'banned' = -1)
Enum16('a' = 1, 'b' = 30000)
```

Um decodificador pode remover o sufixo de parâmetros `(...)` e fazer o encaminhamento como `Int8` / `Int16`, já que os bytes no wire são apenas o índice inteiro. Um cliente que exibe o rótulo interpreta o map `'name' = value` a partir da string do tipo e o mantém junto à coluna, pois o inteiro sozinho não permite recuperar o rótulo. A saída em formato de texto exibe o rótulo (`active`) em vez do índice, entre aspas simples (`'active'`) quando o enum está aninhado em um tipo composto. Como o map não pode ser recuperado a partir da coluna de inteiros, ele deve ser preservado para enums aninhados, como `Array(Enum8(...))` ou `Map(Enum16(...), V)`.

Uma coluna `Enum8('active' = 1, 'inactive' = 2)` com os valores `[active, inactive, active]`:

```text theme={null}
01 02 01
```

Um valor `Enum16(...)` igual a `30000`:

```text theme={null}
30 75                    Int16 LE = 30000
```

#### Decimal(P, S)

Um inteiro com sinal escalado por uma potência de 10. A largura em bytes do inteiro é determinada pela **precisão** `P`; a **escala** `S` é o expoente negativo (o número de dígitos após o separador decimal). Ambos fazem parte da string do tipo.

| Precisão (P) | Inteiro subjacente | Bytes |
| - | - | - |
| 1 ≤ P ≤ 9 | Int32 | 4 |
| 10 ≤ P ≤ 18 | Int64 | 8 |
| 19 ≤ P ≤ 38 | Int128 | 16 |
| 39 ≤ P ≤ 76 | Int256 | 32 |

A codificação no wire é o inteiro subjacente em complemento de dois, little-endian, e o valor decimal lógico é `wire_integer × 10^(-S)`.

O ClickHouse sempre emite `Decimal(P, S)`, independentemente de como o tipo foi declarado. `Decimal32(S)`, `Decimal64(S)` e os demais são todos normalizados para `Decimal(P, S)` no wire (com `P` definido como o máximo natural para a largura: 9, 18, 38, 76). Um decodificador que reconheça apenas `Decimal(P, S)` cobre todas as grafias que o servidor emite.

Valor `123.4567` do tipo `Decimal(9, 4)` → inteiro subjacente `1234567`:

```text theme={null}
87 D6 12 00              Int32 LE = 1234567
```

Valor `-1.5` do tipo `Decimal(18, 1)` → inteiro subjacente `-15`:

```text theme={null}
F1 FF FF FF FF FF FF FF  Int64 LE = -15
```

Valor `123.4567` do tipo `Decimal(38, 4)` (16 bytes no total):

```text theme={null}
87 D6 12 00 00 00 00 00 00 00 00 00 00 00 00 00
```

#### Nothing

O tipo `Nothing` não carrega nenhum valor. Na prática, ele aparece apenas como o tipo interno de `Nullable(Nothing)` — o que o servidor retorna para uma expressão como `SELECT NULL`, cujo único valor válido é a ausência de valor. Conceitualmente, trata-se de um tipo unitário.

No wire, ocupa exatamente **um byte de preenchimento por linha**. O servidor emite o caractere ASCII `'0'` (`0x30`), mas o desserializador ignora esses bytes — o conteúdo é indefinido e os decodificadores não devem depender de nenhum valor específico. O número de bytes gravados é `num_rows × 1`, portanto o `num_rows` do cabeçalho da coluna determina inteiramente quantos bytes devem ser consumidos.

O byte por linha preserva a invariante do bloco: toda coluna ocupa um comprimento derivável de `num_rows`, de modo que os decodificadores leem sequencialmente, sem prefixos de comprimento por célula. O `Nullable` envolvente sempre indica todas as posições como NULL, portanto os marcadores nunca são inspecionados.

Uma coluna `Nullable(Nothing)` com 3 linhas (todas NULL):

```text theme={null}
01 01 01                 null map: 1, 1, 1 (three NULLs)
30 30 30                 Nothing placeholder bytes (one per row)
```

O prefixo do mapa de nulos é o enquadramento padrão de `Nullable` (consulte [Nullable](#nullable)); os três bytes internos correspondem à carga útil de `Nothing`, que o decodificador ignora.

### Tipos de comprimento variável

Cada valor carrega o próprio comprimento no wire.

#### String

String do tipo: `String`. Uma coluna `String` tem dois layouts wire, selecionados pela revisão do protocolo (consulte [O que a revisão controla](#what-the-revision-gates)). A revisão é aquela negociada no protocolo TCP nativo e, para a saída `FORMAT Native`, é `client_protocol_version` (padrão `0`); a entrada `FORMAT Native` usa sempre a revisão `0`, e o formato `Buffers` sempre usa o layout por valor.

**Layout por valor** — abaixo de `DBMS_MIN_REVISION_WITH_STRING_WITH_SIZE_STREAM_SERIALIZATION` (`54492`), o que inclui a revisão padrão `0` de `FORMAT Native`: uma sequência de `num_rows` sequências de bytes prefixadas pelo comprimento:

```text theme={null}
[VarUInt: byte_length] [byte_length bytes: raw value]
[VarUInt: byte_length] [byte_length bytes: raw value]
...
```

Não há separadores entre as linhas além dos prefixos de comprimento, nem estado por linha. Uma string vazia corresponde a um único byte `0x00`. O total de bytes ocupados pela coluna é `Σ (varuint_size(len_i) + len_i)` somado sobre todas as linhas.

Uma coluna com 3 strings `["ab", "", "c"]` (6 bytes no total):

```text theme={null}
02 61 62                 row 0: length 2, "ab"
00                       row 1: length 0, empty
01 63                    row 2: length 1, "c"
```

**Layout de offsets** — a partir da revisão `54492`: dois fluxos concatenados, primeiro os offsets cumulativos em bytes e, em seguida, todos os dados:

```text theme={null}
[UInt64 × num_rows: cumulative byte offset (end of each value), little-endian]
[last offset bytes: all values concatenated, no separators]
```

Os offsets são enviados sem alterações, da mesma forma que um `Array` envia seus offsets pelo protocolo nativo. O offset `i` é a posição final do valor `i` no blob de dados; assim, o tamanho do valor `i` é `offset[i] - offset[i-1]` (com `offset[-1] = 0`) e o último offset corresponde ao tamanho total do blob de dados. Um decodificador lê `8 × num_rows` bytes de offsets e, em seguida, lê todo o blob de dados de uma só vez, usando o último offset como comprimento exato. Isso permite pré-alocar o buffer com precisão e fazer cópia em massa em vez de parsing linha a linha, além de dispensar a conversão de tamanhos em offsets. Uma coluna com `num_rows = 0` não contribui com nenhum byte em nenhum dos fluxos.

As mesmas 3 strings `["ab", "", "c"]` (27 bytes no total):

```text theme={null}
02 00 00 00 00 00 00 00  row 0 offset: 2  (end of "ab")
02 00 00 00 00 00 00 00  row 1 offset: 2  (end of "", so unchanged)
03 00 00 00 00 00 00 00  row 2 offset: 3  (end of "c")
61 62 63                 "ab" + "" + "c"
```

Quando o layout de offsets está em vigor, ele se aplica a toda `String` no bloco, incluindo `String` aninhada em tipos compostos (`Array(String)`, `Nullable(String)`, `Map`, `Tuple`, `Variant`, `Dynamic`, `JSON`), com uma exceção: o dicionário de uma coluna [`LowCardinality`](#lowcardinality) é sempre gravado com a serialização aninhada padrão, de modo que seus valores `String` mantêm o layout por valor em todas as revisões. Isso abrange `LowCardinality(String)`, `LowCardinality(Nullable(String))` e qualquer outro dicionário `LowCardinality` que contenha String.

Em ambos os layouts, a `String` do ClickHouse é orientada a bytes, e não a texto: a validade UTF-8 não é verificada, e um valor pode conter quaisquer bytes, incluindo NUL embutido. Um decodificador que mapeie para um tipo de string UTF-8 deve validar os dados na leitura ou expor os bytes brutos ao chamador.

#### FixedString(N)

String do tipo: `FixedString(N)`, em que `N` é um inteiro positivo (por exemplo, `FixedString(16)`). A coluna tem exatamente `N × num_rows` bytes brutos, sem prefixos de comprimento e sem separadores. Um decodificador interpreta `N` a partir da string do tipo e consome essa quantidade de bytes por linha.

Quando o SQL faz o insert de um valor com menos de `N` bytes (por exemplo, `CAST('abc' AS FixedString(5))`), o servidor preenche à direita com bytes NUL (`0x00`) até atingir o comprimento declarado. Esses bytes de preenchimento fazem parte do valor armazenado e são enviados no wire como estão; removê-los é responsabilidade do cliente. Assim como `String`, `FixedString(N)` se comporta mais como um array de bytes do que como texto — normalmente é usado para identificadores de largura fixa, bytes de endereço ou digests de hash.

Dois valores `FixedString(3)` `["abc", "de\0"]` (6 bytes no total):

```text theme={null}
61 62 63                 row 0: 3 bytes, "abc"
64 65 00                 row 1: 3 bytes, "de" + NUL padding
```

Comparação entre os dois tipos de string:

| Propriedade | `String` | `FixedString(N)` |
| - | - | - |
| Prefixo de comprimento por linha | Sim (VarUInt) ou, a partir da revisão `54492`+, um fluxo de offsets separado | Não |
| Tamanho da linha | Variável | Exatamente `N` bytes |
| Total de bytes da coluna | Variável | `N × num_rows` |
| Preenchimento com bytes NUL | n/a | Preenchido à direita pelo servidor |
| UTF-8 esperado | Normalmente (sem imposição) | Não (tratar como bytes brutos) |
| Parâmetro de tipo | Nenhum | Inteiro `N` obrigatório |

### Tipos compostos

Os tipos compostos encapsulam um ou mais tipos internos e compartilham um modelo wire comum: **múltiplos fluxos por coluna**. Uma única coluna lógica é codificada como duas ou mais sequências de bytes, lidas de forma independente e concatenadas.

Eles compartilham três propriedades estruturais:

* **Forma fixa por schema.** A estrutura é determinada inteiramente pela string do tipo no momento da decodificação. `Array(UInt32)` sempre tem o mesmo layout de fluxos, de um bloco para outro.
* **Sem prefixo de versão próprio.** O wrapper composto em si não adiciona nenhum byte de versão; seu enquadramento (offsets, mapa de nulos, fluxos de elementos) permanece estável entre os lançamentos do ClickHouse. Isso vale apenas para o *wrapper*. Para tipos internos versionados, consulte a observação sobre a fase de prefixo abaixo.
* **Sem estado próprio entre blocos.** O enquadramento do wrapper é totalmente autodescritivo em cada bloco; qualquer questão de estado entre blocos decorre de um tipo interno versionado, e não do wrapper.

Os tipos compostos são recursivos: um tipo interno pode ser, ele próprio, um tipo composto.

**Fase de prefixo antes dos fluxos de dados.** A leitura de uma coluna ocorre em duas fases, nesta ordem: uma **fase de prefixo de estado** e, em seguida, a **fase de fluxos de dados**. Um wrapper composto não tem bytes de prefixo próprios, mas *delega* a fase de prefixo à sua serialização interna antes de gravar qualquer um de seus próprios fluxos de dados: `SerializationArray` executa a fase de prefixo do seu tipo interno antes de gravar os offsets do array, e `Tuple`, `Map`, `Nested` e `Nullable` fazem o mesmo por meio das serializações de seus elementos (`Nullable` executa o prefixo interno antes do seu mapa de nulos).

Portanto, quando um tipo composto encapsula um [tipo versionado/com estado](#versioned-types) (`LowCardinality`, `Variant`, `Dynamic`, `JSON`), o prefixo de versão/estado desse tipo interno é emitido *primeiro*, antes dos offsets e da carga útil de elementos do wrapper. Por exemplo, `Array(LowCardinality(String))` é organizado como `[LowCardinality state prefix]` → `[array offsets]` → `[flattened LowCardinality element payload]`, e não com os offsets primeiro.

Um decodificador que lê os offsets antes de executar a fase de prefixo interna perderá a sincronização em qualquer tipo composto que contenha `LowCardinality`, `Variant`, `Dynamic` ou `JSON`. Quando todos os tipos internos são folhas simples ou outros compostos não versionados, a fase de prefixo não emite nenhum byte, e a descrição com offsets primeiro, apresentada abaixo, se aplica sem alterações.

#### Nullable(T)

String do tipo: `Nullable(InnerType)`. Exemplos: `Nullable(UInt32)`, `Nullable(String)`, `Nullable(FixedString(16))`, `Nullable(DateTime('UTC'))`.

Assim como os demais tipos compostos, `Nullable` delega a [fase de prefixo](#composite-types) à serialização do seu tipo interno antes da gravação do mapa de nulos: quando o tipo interno é versionado, o prefixo de estado do tipo interno é emitido **primeiro**. Portanto, `Nullable(Tuple(LowCardinality(String)))` começa com o prefixo de estado de `LowCardinality`, e não com o mapa de nulos. Quando o tipo interno é um tipo folha ou outro tipo não versionado, a fase de prefixo não emite nenhum byte.

O layout wire consiste na fase de prefixo do tipo interno (vazia, a menos que o tipo interno seja versionado), seguida de dois fluxos concatenados, começando pelo mapa de nulos:

```text theme={null}
[inner type's state prefix]   empty for leaf/non-versioned inners; emitted first when the inner is versioned
[null-map stream]             num_rows × UInt8
[values stream]               inner type's encoding for num_rows values
```

O mapa de nulos tem exatamente `num_rows` bytes, um por linha:

| Valor do byte | Significado |
| - | - |
| `0x00` | O valor está presente nesta linha. |
| diferente de zero (canônico `0x01`) | O valor é NULL. Os bytes correspondentes no fluxo de valores são um marcador. |

O fluxo de valores contém a codificação padrão do tipo interno para **todas** as `num_rows` linhas, incluindo as posições nulas. Um decodificador ainda precisa ler os bytes de marcador nas posições nulas para avançar no fluxo, mas deve consultar o mapa de nulos antes de interpretar qualquer valor individual. Os remetentes podem gravar quaisquer bytes nas posições nulas; portanto, os decodificadores não devem depender de um valor específico de marcador.

Valores de marcador por família de tipo interno:

| Família de tipo interno | Marcador na posição nula |
| - | - |
| Largura fixa (UInt/Int/Float/DateTime/UUID/etc.) | Bytes inicializados com zero, com a largura do tipo |
| `String` | String vazia — um único byte `0x00` |
| `FixedString(N)` | `N` bytes zero |
| `Array(T)` | Array vazio — os offsets avançam zero |
| `Tuple(T1, T2, ...)` | Cada elemento usa seu próprio marcador |

`Nullable(T)` pode aparecer dentro de `Array`, `Tuple`, `Map` e `Nested` — `Array(Nullable(T))` e `Tuple(Nullable(T1), T2)` são comuns. A nulabilidade não pode ser aninhada em si mesma: `Nullable(Nullable(T))` é rejeitado pelo servidor.

Um `Nullable(UInt8)` com três linhas `[5, NULL, 9]` (6 bytes no total):

```text theme={null}
00 01 00                 null-map: present, null, present
05 00 09                 values:   5, placeholder, 9
```

Um `Nullable(String)` com três linhas `["hello", NULL, "world"]` (15 bytes no total):

```text theme={null}
00 01 00                 null-map
05 'h' 'e' 'l' 'l' 'o'   row 0: "hello"
00                       row 1: placeholder (empty string)
05 'w' 'o' 'r' 'l' 'd'   row 2: "world"
```

#### Array(T)

String do tipo: `Array(InnerType)`. Exemplos: `Array(UInt32)`, `Array(String)`, `Array(Nullable(UInt32))`, `Array(Array(UInt8))`.

O layout wire é composto pela [fase de prefixo](#composite-types) do tipo interno (vazia, a menos que o tipo interno seja versionado), seguida de dois fluxos concatenados, começando pelos offsets:

```text theme={null}
[inner type's state prefix]   empty for leaf/non-versioned inners; emitted first when the inner is versioned
[offsets stream]              num_rows × UInt64 LE
[values stream]               inner type's encoding for offsets[num_rows - 1] values
```

O fluxo de offsets consiste em exatamente `num_rows` valores UInt64 little-endian, cada um representando a **posição final cumulativa** no fluxo de valores após os elementos daquela linha:

* Índice inicial dos elementos da linha `N` = `offsets[N - 1]` (ou `0` quando `N == 0`).
* Índice final dos elementos (exclusivo) da linha `N` = `offsets[N]`.
* Número de elementos da linha `N` = `offsets[N] - offsets[N - 1]`.

Portanto, `offsets[num_rows - 1]` é o número total de elementos em todas as linhas, e o fluxo de valores contém exatamente essa quantidade de valores internos, concatenados em sequência.

Os offsets são **monotonicamente não decrescentes**; offsets consecutivos iguais indicam uma linha vazia, e um decodificador deve rejeitar offsets não monotônicos, tratando-os como dados corrompidos. Uma coluna vazia (`num_rows == 0`) grava zero bytes, ou seja, não há fluxo de offsets nem fluxo de valores. Os tipos internos podem ser de qualquer tipo, inclusive outros tipos compostos: `Array(Array(T))`, `Array(Tuple(...))` e `Array(Nullable(T))` são todos válidos.

`Array(UInt32)` com as linhas `[[10, 20, 30], [], [40, 50]]` (44 bytes no total):

```text theme={null}
Offsets (3 × UInt64 LE = 24 bytes):
03 00 00 00 00 00 00 00      offsets[0] = 3
03 00 00 00 00 00 00 00      offsets[1] = 3 (empty row)
05 00 00 00 00 00 00 00      offsets[2] = 5

Values (5 × UInt32 LE = 20 bytes):
0A 00 00 00                  10
14 00 00 00                  20
1E 00 00 00                  30
28 00 00 00                  40
32 00 00 00                  50
```

Cada offset é o *fim* cumulativo do trecho de uma linha no fluxo de valores compartilhado; o início é o offset anterior (ou `0` para a linha 0). Offsets consecutivos iguais indicam uma linha vazia:

![Offsets mapeando linhas em um stream de valores compartilhado](https://mintcdn.com/private-7c7dfe99/kA9td9fgr8TRWXKS/images/interfaces/native-format/array-offsets.webp?fit=max\&auto=format\&n=kA9td9fgr8TRWXKS\&q=85\&s=caba90e4578ba0cd297f97007d747b98)

`Array(String)` com as linhas `[["a", "bb"], []]` (20 bytes no total):

```text theme={null}
Offsets (2 × UInt64 LE = 16 bytes):
02 00 00 00 00 00 00 00      offsets[0] = 2
02 00 00 00 00 00 00 00      offsets[1] = 2 (empty row)

Values (2 strings, 4 bytes total):
01 'a'                       row's first string: "a"
02 'b' 'b'                   row's second string: "bb"
```

`Array(Array(UInt32))` com as linhas `[[[1,2]], [], [[3], [4,5]]]` aninha a mesma estrutura:

* Offsets externos: `[1, 1, 3]` — a linha 0 tem 1 array interno, a linha 1 tem 0 e a linha 2 tem 2.
* O `Array(UInt32)` intermediário decodifica 3 linhas com offsets `[2, 3, 5]`.
* O `UInt32` mais interno decodifica 5 valores: `[1, 2, 3, 4, 5]`.

No total, são 24 (offsets externos) + 24 (offsets intermediários) + 20 (valores) = 68 bytes.

#### Tuple(T1, T2, ...)

String do tipo: `Tuple(T1, T2, ..., Tn)`. Exemplos: `Tuple(UInt32, String)`, `Tuple(Int32)`, `Tuple(Array(UInt32), String)`, `Tuple(UInt8, Tuple(Int32, String))`. O ClickHouse também oferece suporte a **tuplas nomeadas** por meio de `Tuple(a UInt32, b String)`; os nomes são apenas metadados e não afetam o formato wire.

O layout wire consiste na [fase de prefixo](#composite-types) dos elementos (cada elemento versionado contribui com seu prefixo de estado, na ordem de declaração; a fase fica vazia para elementos não versionados), seguida de *N* fluxos concatenados, um por tipo de elemento, na ordem de declaração:

```text theme={null}
[element state prefixes]   in declaration order; empty unless an element type is versioned
[stream for T1]    inner T1's encoding for num_rows values
[stream for T2]    inner T2's encoding for num_rows values
 ...
[stream for Tn]    inner Tn's encoding for num_rows values
```

Cada fluxo codifica exatamente `num_rows` valores. Não há prefixo de comprimento, nem fluxo de offsets, nem separadores entre os fluxos. Uma coluna vazia (`num_rows == 0`) grava zero bytes por fluxo. Os elementos podem ser de qualquer tipo, inclusive outros tipos compostos — `Tuple(Tuple(...), ...)`, `Tuple(Array(...), ...)` e `Tuple(Nullable(T1), T2)` são todos válidos.

A tupla sem elementos `Tuple()` também é válida — ela surge de expressões como `SELECT tuple()` ou `CAST(x AS Tuple())`. Como não possui fluxos de elementos, ela é serializada da mesma forma que [Nothing](#nothing): **um byte marcador (`0x30`, ASCII `'0'`) por linha**, que o desserializador descarta. A contagem de linhas vem do cabeçalho do bloco, exatamente como no caso de `Nothing`.

`Tuple(UInt8, UInt8)` com 3 linhas `(1,4), (2,5), (3,6)`:

```text theme={null}
Element 0 stream (3 × UInt8 = 3 bytes):
01 02 03

Element 1 stream (3 × UInt8 = 3 bytes):
04 05 06
```

O layout **não** é orientado a linhas (row-major): ao ler de volta os bytes brutos, obtém-se `[1, 2, 3]` para o elemento 0 e `[4, 5, 6]` para o elemento 1.

`Tuple(UInt32, String)` com 2 linhas `(10, "a")` e `(20, "bb")` (13 bytes no total):

```text theme={null}
Element 0 stream (2 × UInt32 LE = 8 bytes):
0A 00 00 00                  10
14 00 00 00                  20

Element 1 stream (2 strings, 5 bytes total):
01 'a'                       "a"
02 'b' 'b'                   "bb"
```

#### Map(K, V)

String do tipo: `Map(KeyType, ValueType)`. Exemplos: `Map(String, UInt32)`, `Map(String, Array(UInt32))`, `Map(UInt8, Tuple(Int32, String))`, `Map(Array(String), Int8)`. O formato wire não impõe restrição a nenhum dos dois tipos — tanto `K` quanto `V` podem ser qualquer tipo compatível, inclusive tipos compostos. (As regras do ClickHouse no nível SQL sobre os tipos de chave aceitos variaram entre lançamentos; consulte a documentação de SQL referente à versão do servidor de destino.)

O layout wire é idêntico byte a byte ao de `Array(Tuple(K, V))`; portanto, começa com a [fase de prefixo](#composite-types) interna (vazia, a menos que `K` ou `V` seja versionado):

```text theme={null}
[K/V state prefixes]   from the inner Tuple's prefix phase; empty unless K or V is versioned
[offsets stream]    num_rows × UInt64 LE                   ← from Array
[keys stream]       K's encoding for total_pairs values    ┐ from Tuple's
[values stream]     V's encoding for total_pairs values    ┘ per-element streams
```

onde `total_pairs = offsets[num_rows - 1]` (ou `0` quando `num_rows == 0`). O fluxo de offsets tem a mesma semântica de [Array](#array). As chaves são alinhadas posicionalmente com os valores: o par `i` é `(keys[i], values[i])`.

No ClickHouse, a representação em memória de uma coluna Map é um array de tuplas; o sistema de tipos a expõe como um tipo distinto para facilitar o uso em SQL (`m['key']`, `mapKeys`, `mapValues`). O formato wire é uma serialização direta desse armazenamento, de modo que `Map` e `Array(Tuple(K, V))` são intercambiáveis byte a byte.

Os offsets são monotonicamente não decrescentes, e tanto o fluxo de chaves quanto o fluxo de valores contêm exatamente `total_pairs` valores. Uma coluna vazia não grava nenhum byte. Em uma mesma linha, as chaves normalmente são únicas, mas essa é uma regra semântica, e não uma imposição do formato wire: o formato wire permite que chaves duplicadas sobrevivam à ida e volta, e a semântica do lado do servidor só resolve duplicatas quando uma função compatível com Map consome a linha.

`Map(UInt8, UInt8)` com 2 linhas `{1:10, 2:20}`, `{3:30}` (22 bytes no total):

```text theme={null}
Offsets (2 × UInt64 LE = 16 bytes):
02 00 00 00 00 00 00 00      offsets[0] = 2
03 00 00 00 00 00 00 00      offsets[1] = 3

Keys (3 × UInt8 = 3 bytes):
01 02 03                     keys: 1, 2, 3

Values (3 × UInt8 = 3 bytes):
0A 14 1E                     values: 10, 20, 30
```

Chaves e valores são armazenados em fluxos separados, sem intercalação — o par `i` é reconstruído pela leitura conjunta de `keys[i]` e `values[i]`.

`Map(String, UInt32)` com 1 linha `{'a':1, 'b':2}` (20 bytes no total):

```text theme={null}
Offsets (1 × UInt64 LE = 8 bytes):
02 00 00 00 00 00 00 00      offsets[0] = 2

Keys (2 strings, 4 bytes total):
01 'a'                       "a"
01 'b'                       "b"

Values (2 × UInt32 LE = 8 bytes):
01 00 00 00                  1
02 00 00 00                  2
```

#### Nested(name1 T1, name2 T2, ...)

A representação on-wire de `Nested` depende da configuração `flatten_nested` do lado do servidor, o que dá origem a dois casos distintos.

![Layout de coluna composta com e sem achatamento](https://mintcdn.com/private-7c7dfe99/kA9td9fgr8TRWXKS/images/interfaces/native-format/nested-layout.webp?fit=max\&auto=format\&n=kA9td9fgr8TRWXKS\&q=85\&s=786631c14f4e8b40cbd3ad9d47eb3c56)

**Caso A: `flatten_nested = 1` (padrão do servidor).** Quando a tabela é criada com as configurações padrão, `Nested` **não é um wire type**. O servidor armazena e apresenta a coluna como N colunas `Array(T_i)` paralelas com **nomes separados por ponto** (`outer.field1`, `outer.field2` e assim por diante). Para a camada de formato, não há nada de novo: cada coluna com ponto no nome é um [Array](#array) comum:

```text theme={null}
DESCRIBE TABLE t   -- t has column n Nested(a UInt8, b String)
id     UInt8
n.a    Array(UInt8)
n.b    Array(String)
```

**Caso B: `flatten_nested = 0`.** Quando a tabela é criada com `flatten_nested = 0`, a coluna aparece no wire como uma única coluna com a string do tipo `Nested(name1 T1, name2 T2, ...)`, e seu layout após a string do tipo é **idêntico byte a byte a `Array(Tuple(T1, T2, ..., Tn))`** — incluindo a [fase de prefixo](#composite-types) interna. Assim, qualquer campo versionado `T_i` emite primeiro seu prefixo de estado, antes dos offsets. O exemplo abaixo usa campos não versionados, portanto a fase de prefixo fica vazia:

```text theme={null}
Nested(a UInt8, b String) bytes (after type string):
  02 00 00 00 00 00 00 00       offsets[0] = 2
  03 00 00 00 00 00 00 00       offsets[1] = 3
  0A 14 1E                       UInt8 stream
  01 'x' 01 'y' 01 'z'           String stream

Array(Tuple(a UInt8, b String)) bytes (after type string):
  02 00 00 00 00 00 00 00       offsets[0] = 2
  03 00 00 00 00 00 00 00       offsets[1] = 3
  0A 14 1E                       UInt8 stream
  01 'x' 01 'y' 01 'z'           String stream
```

A única diferença está no texto da string do tipo: `Nested` preserva os nomes dos campos (`a`, `b`), que `Array(Tuple)` não mantém como slots nomeados.

A string do tipo do Caso B é uma lista de pares (nome, tipo) separados por vírgulas. O primeiro espaço em branco separa o nome do tipo; o próprio tipo pode conter outros espaços em branco, vírgulas e parênteses, então o parsing exige o mesmo divisor sensível à profundidade usado para `Tuple`. O layout wire:

```text theme={null}
[offsets stream]    num_rows × UInt64 LE                       ← from Array
[field1 stream]     T1's encoding for total_elements values    ┐ from Tuple's
[field2 stream]     T2's encoding for total_elements values    │ per-element
 ...                                                            │ streams
[fieldn stream]     Tn's encoding for total_elements values    ┘
```

onde `total_elements = offsets[num_rows - 1]` (ou `0` quando `num_rows == 0`). Os offsets são monotonicamente não decrescentes, e cada fluxo de campo contém exatamente `total_elements` valores. No momento do INSERT, o servidor garante que, dentro de uma mesma linha, todos os campos tenham o mesmo número de elementos. Uma coluna vazia não grava nenhum byte.

`Nested(a UInt8, b String)` com 2 linhas `[(10,'x'),(20,'y')]` e `[(30,'z')]` (25 bytes após a string do tipo):

```text theme={null}
Offsets (2 × UInt64 LE = 16 bytes):
02 00 00 00 00 00 00 00      offsets[0] = 2
03 00 00 00 00 00 00 00      offsets[1] = 3

Field 'a' stream (3 × UInt8 = 3 bytes):
0A 14 1E                     10, 20, 30

Field 'b' stream (3 strings, 6 bytes):
01 'x' 01 'y' 01 'z'         "x", "y", "z"
```

### Aliases de tipos

Vários tipos são apenas aliases: o servidor envia o nome do alias no cabeçalho da coluna, mas os bytes seguintes são os de um tipo subjacente. Um decodificador mapeia o alias para esse tipo e reutiliza seu codec — nenhum novo formato wire está envolvido.

Os tipos geográficos são aliases de arrays e tuplas aninhados:

| String do tipo | Tipo wire subjacente |
| - | - |
| `Point` | `Tuple(Float64, Float64)` |
| `Ring`, `LineString`, `MultiPoint` | `Array(Point)` |
| `Polygon`, `MultiLineString` | `Array(Ring)` |
| `MultiPolygon` | `Array(Polygon)` |

Assim, uma coluna `Point` é decodificada exatamente como `Tuple(Float64, Float64)` (renderizada como `(1,2)`), um `Ring` como `Array(Tuple(Float64, Float64))` (`[(0,0),(1,1)]`), e assim por diante, subindo na hierarquia.

`Geometry` também é um alias, mas de um [`Variant`](#variant), e não de um array aninhado: sua carga útil é o variant dos sete tipos geográficos acima. O cabeçalho da coluna traz apenas a string do tipo `Geometry` — ele **não** detalha o variant —, portanto o decodificador precisa expandi-lo por conta própria. Diferentemente de um `Variant` declarado explicitamente pelo usuário, os discriminadores de `Geometry` **não** seguem a ordem crescente por nome: eles são fixos por questões de compatibilidade, e novos tipos geográficos são sempre anexados ao final. O mapeamento é `0` = `LineString`, `1` = `MultiLineString`, `2` = `MultiPolygon`, `3` = `Point`, `4` = `Polygon`, `5` = `Ring`, `6` = `MultiPoint` (adicionado na versão 26.7 — fluxos de servidores mais antigos simplesmente nunca contêm o discriminador `6`). Cada valor selecionado é então decodificado por meio do respectivo alias geográfico acima (`NULL` usa o discriminador `NULL` do `Variant`, `255`).

`SimpleAggregateFunction(func, T)` é um alias para seu tipo de valor `T`. Ele armazena um valor agregado já finalizado, de modo que sua forma no wire e sua renderização são exatamente as de `T` (`SimpleAggregateFunction(sum, UInt64)` é decodificado como `UInt64`). Somente a forma com um único tipo de valor funciona como alias dessa maneira; o próprio tipo subjacente pode ser composto.

> [!NOTE]
>
> Dois tipos relacionados **não** são aliases. Eles são tipos de coluna `Native` válidos — um cliente pode receber uma coluna `AggregateFunction` de um combinador `-State` ou de uma agregação distribuída, por exemplo —, mas cada um carrega sua própria carga útil especializada, que está fora do escopo desta página:
>
> * `AggregateFunction(func, ...)` contém um estado de agregação *intermediário* (não um valor finalizado); seu layout binário é específico da função de agregação e da versão. O nome da função pode conter parâmetros, cada um impresso como um literal SQL, como em `AggregateFunction(topK(3), UInt64)`. Algumas funções de agregação imprimem um sufixo `::Type` explícito em um parâmetro, como em `AggregateFunction(topK('3'::Decimal32(0)), UInt64)`, porque um valor `Decimal` ou inteiro largo não tem uma forma literal simples que possa ser impressa; fazer isso ou não é uma propriedade de cada função, e não do formato, portanto um decodificador que inspeciona parâmetros deve aceitar ambas as grafias. Um decodificador que precisa apenas dos tipos dos argumentos não é afetado. Uma função de agregação versionada explicita uma versão de estado diferente de zero como primeiro argumento da string do tipo — `AggregateFunction(1, quantileDeterministic, UInt64, UInt64)` —, enquanto a versão `0` não é impressa, de modo que a string do tipo continua sendo `AggregateFunction(quantileDeterministic, UInt64, UInt64)`. Em uma conexão negociada (revisão acima de `0`), o componente de gravação escolhe a versão **somente** com base na revisão do protocolo — nunca com base em uma versão que porventura esteja fixada em seu tipo local, porque a versão `0` é indistinguível, no wire, de "nenhuma versão anunciada" —, portanto a mesma função pode anunciar versões diferentes em conexões diferentes, e um leitor deve obter a versão da string do tipo em vez de presumir a mais recente. Na revisão `0` não há peer para o qual derivar uma versão — o fluxo é autodescritivo e lido de volta por quem o gravou —, então uma versão fixada no tipo é mantida no fluxo (e na string do tipo anunciada) em vez de ser rebaixada para `0`. `quantileDeterministic` (incluindo as grafias `quantilesDeterministic` e `medianDeterministic`) é versionada a partir da revisão `54491`: a versão `1` acrescenta um `UInt8 skip_degree` ao final do estado, e seu prefixo é idêntico byte a byte ao da versão `0`. Abaixo dessa revisão, o componente de gravação emite estados na versão `0`, de modo que um peer mais antigo não é afetado. `uniq` é versionada a partir da revisão `54493`: a versão `1` acrescenta um byte de flags no início do layout da versão `0` e, quando o bit `0` das flags está definido, acrescenta ao final uma pequena amostra adicional de hashes completos de 64 bits, cada um combinado via XOR com a constante `0x9E3779B97F4A0000` (`UInt8 skip_degree`, `VarUInt count` e, em seguida, os valores de 8 bytes), o que mantém a estimativa precisa em cardinalidades acima de dois bilhões; um estado na versão `0` carrega apenas a amostra principal de 32 bits. O bit `1` das flags indica que a amostra de 64 bits está *incompleta* — o estado absorveu um estado na versão `0`, cujos elementos nunca foram oferecidos a uma amostra de 64 bits —, caso em que nenhuma amostra de 64 bits é gravada e um leitor deve estimar somente a partir da amostra principal de 32 bits (e continuar indicando isso nos estados que vier a gravar); os dois bits são, portanto, mutuamente exclusivos, e um leitor deve rejeitar um estado que defina ambos. Nenhum outro bit é definido, e um leitor deve rejeitar um estado que defina algum deles. `uniq` is versioned from revision `54493`: version `1` prepends a flags byte to the version-`0` layout and, when bit `0` of the flags is set, appends a small additional sample of full 64-bit hashes, each XOR-ed with the constant `0x9E3779B97F4A0000` (`UInt8 skip_degree`, `VarUInt count`, then the 8-byte values), which keeps the estimate accurate at cardinalities above two billion; a version `0` state carries the 32-bit main sample only. Bit `1` of the flags says that the 64-bit sample is *incomplete* — the state has absorbed a version `0` state, whose elements were never offered to a 64-bit sample — in which case no 64-bit sample is written and a reader must estimate from the 32-bit main sample alone (and keep saying so in the states it goes on to write); the two bits are therefore mutually exclusive, and a reader must reject a state that sets both. No other bit is defined, and a reader must reject a state that sets one. Tudo isso se aplica igualmente a um estado aninhado em um tipo contêiner — `Array`, `Tuple`, `Map`, `Nullable`, `Variant` (em que a versão explicitada da alternativa altera a string do tipo sem alterar a ordem do discriminador) ou um wrapper `Nested` / `SimpleAggregateFunction`. Isso também vale para combinadores de funções de agregação: um combinador de repasse aplicado a uma função versionada — `uniqIf`, `uniqArray`, `uniqForEach`, `uniqMap`, `uniqArgMin` / `uniqArgMax`, `uniqOrNull` / `uniqOrDefault`, `uniqResample` e o adaptador implícito de argumento `Nullable` — repassa a versão aos estados aninhados contidos em sua carga útil, explicita a versão atual da função aninhada na string do tipo de um estado recém-criado exatamente como a função simples (`AggregateFunction(1, uniqIf, UInt64, UInt8)`) e, em uma conexão negociada, deriva a versão da revisão do protocolo da mesma forma. O nome personalizado de um wrapper contém uma segunda cópia do seu tipo de argumento, portanto o componente de gravação deve reconstruir essa cópia com a mesma versão de estado de agregação negociada; caso contrário, seu cabeçalho pode anunciar a versão `1` para uma carga útil na versão `0`. Um caso extremo de `Variant` não tem nenhuma representação no wire: quando duas alternativas diferem apenas em uma versão de estado explicitada (o que é válido como tipo de coluna, já que as strings do tipo diferem), reversionar ambas para a revisão negociada as fundiria em uma única alternativa, portanto o remetente deve abortar a transferência em vez de anunciar versões diferentes daquelas com que a carga útil foi gravada. `Dynamic` é a exceção deliberada ao versionamento derivado da revisão: uma coluna `Dynamic` anuncia por conta própria seus tipos aninhados — por meio de suas próprias listas de tipos e da [codificação binária de tipos](https://clickhouse.com/docs/pt-BR/reference/data-types/data-types-binary-encoding.md), cujo campo de versão explícito carrega uma versão de estado explicitada (os valores de sua variante compartilhada incorporam o tipo dessa forma mesmo em repouso) —, e esses anúncios fazem parte dos próprios dados da coluna, idênticos para qualquer peer e qualquer meio, portanto nunca são reescritos para uma revisão negociada. Assim, um estado aninhado em um valor `Dynamic` é gravado na versão explicitada em seu tipo (que um estado recém-criado carrega, já que o resultado de uma consulta `-State` explicita sua versão) e na versão `0` quando o tipo não explicita nenhuma, da mesma forma em qualquer conexão e meio.
> * `QBit(T, N[, stride])` armazena um vetor com seus planos de bits transpostos para cargas de trabalho de busca vetorial; o layout de seu fluxo on-wire (fluxos de planos de bits `FixedString` ordenados por grupo, totalizando `element_size * (N / stride)` com um `stride` explícito) e sua codificação binária de tipo (tag `0x36`, ou `0x37` `QBitWithStride` quando `stride != N`) estão documentados na [página do tipo de dados `QBit`](https://clickhouse.com/docs/pt-BR/reference/data-types/qbit) e na referência de [codificação binária de tipos](https://clickhouse.com/docs/pt-BR/reference/data-types/data-types-binary-encoding.md), de modo que um leitor `Native` não precisa deduzi-los do código-fonte C++.

### Tipos versionados

Tipos versionados carregam, on-wire, um prefixo de versão de serialização que indica qual variante da codificação vem em seguida. Eles também podem usar múltiplos fluxos (assim como os tipos compostos). No wire `Native`, o prefixo e eventuais dicionários são definidos por bloco — esses tipos não mantêm estado entre blocos (veja [a nota sobre o prefixo por bloco](#serialization-version-concept) abaixo); o estado de serialização entre blocos existe apenas no fluxo em disco do MergeTree.

Esses tipos são consideravelmente mais complexos do que os tipos compostos de formato fixo, e um cliente voltado para consultas analíticas simples pode deixar o suporte a eles para depois.

#### Versão de serialização: conceito

Uma **versão de serialização** é um número de versão on-wire, por tipo e por coluna, que indica qual variante da codificação de um tipo o remetente está usando. Ela é o primeiro elemento do prefixo de estado da coluna; assim, o decodificador a lê e faz o encaminhamento para o parser adequado ao restante da coluna.

Ela não se confunde com a versão do protocolo:

| Dimensão | Versão do protocolo | Versão de serialização (esta seção) |
| - | - | - |
| Escopo | Toda a conexão | Por tipo, por coluna |
| Negociada | Sim, no handshake | Não — o remetente grava, o receptor lê |
| Controla | Quais recursos em nível de pacote estão ativos | Qual variante wire de um tipo |
| Leitura obrigatória | Sim | Sim, para cada coluna versionada |

A maioria dos tipos versionados grava a versão como um UInt64 little-endian imediatamente antes de qualquer outro dado do prefixo de estado; alguns usam VarUInt ou UInt8. O decodificador lê a versão primeiro e rejeita valores desconhecidos — uma versão mais alta indica um formato de remetente mais recente que o decodificador não reconhece, e interpretá-lo incorretamente corrompe todos os bytes seguintes.

O prefixo de estado é emitido no início de **cada bloco cuja contagem de linhas é maior que zero**, imediatamente antes da carga útil desse bloco.

O gravador Native e o leitor **não** mantêm o estado de serialização entre blocos: o `NativeWriter` cria um novo estado de serialização e grava um prefixo de estado para cada bloco de coluna não vazio que escreve, e o `NativeReader` cria um novo estado de desserialização e o lê para cada bloco não vazio que lê (ambos ignoram totalmente o prefixo quando `rows == 0`).

Por isso, blocos de cabeçalho (rows = 0) e blocos vazios não emitem nada, e o decodificador deve ler o prefixo de estado novamente no início de cada bloco não vazio. Um decodificador que lê o prefixo apenas uma vez e trata os blocos seguintes como se contivessem só carga útil lerá o prefixo do próximo bloco como dados e perderá a sincronização:

![Prefixo de estado repetido para cada bloco não vazio](https://mintcdn.com/private-7c7dfe99/kA9td9fgr8TRWXKS/images/interfaces/native-format/state-prefix-per-block.webp?fit=max\&auto=format\&n=kA9td9fgr8TRWXKS\&q=85\&s=753ca07b93f3b88feea72d9584eca779)

#### Referência de versões de serialização

| Tipo | Largura do campo | Valor | Nome | Significado |
| - | - | - | - | - |
| **Object** (base para JSON) | UInt64 LE | `0` | `V1` | Codificação original. Inclui o parâmetro `max_dynamic_paths` e uma lista de caminhos dinâmicos. |
| | | `1` | `STRING` | Modo de compatibilidade do formato Native — o Object é transmitido como uma única coluna `String` contendo texto JSON. |
| | | `2` | `V2` | Layout V1 sem o parâmetro `max_dynamic_paths`. |
| | | `3` | `FLATTENED` | Modo de compatibilidade do formato Native — representação achatada de caminhos. |
| | | `4` | `V3` | Gravado apenas em partes MergeTree, nunca no wire; o layout não é especificado aqui. |
| **Dynamic** | UInt64 LE | `1` | `V1` | Codificação original. Inclui `max_dynamic_types` e uma lista de tipos variantes em tempo de execução. |
| | | `2` | `V2` | V1 sem o parâmetro `max_dynamic_types`. |
| | | `3` | `FLATTENED` | Modo de compatibilidade do formato Native. |
| | | `4` | `V3` | Gravado apenas em partes MergeTree, nunca no wire; o layout não é especificado aqui. |
| Modo de discriminador do **Variant** | UInt64 LE | `0` | `BASIC` | O discriminador de cada linha é gravado literalmente. |
| | | `1` | `COMPACT` | Se todas as linhas de um granule compartilharem o mesmo discriminador, apenas um único valor + marcador de granule é gravado. |
| Formato de granule do **Variant** (quando o modo é `COMPACT`) | UInt8 | `0` | `PLAIN` | O granule tem discriminadores heterogêneos. |
| | | `1` | `COMPACT` | O granule tem um único discriminador para todas as linhas. |
| Serialização de chave do **LowCardinality** | Int64 | `1` | `sharedDictionariesWithAdditionalKeys` | Única versão definida atualmente. |
| Fallback **JSON-as-String** (quando `output_format_native_write_json_as_string` está habilitado) | UInt64 LE | `1` | `JSONStringSerializationVersion` | A coluna JSON chega como uma coluna `String` precedida por este prefixo. |

Alguns pontos importantes sobre a tabela:

* **Os valores não são contíguos.** `Dynamic` usa `1`, `2`, `3` e `4`, com `V3` em `4` e `FLATTENED` em `3`. Um número maior não indica necessariamente uma versão mais recente.
* **Alguns valores existem apenas no formato Native.** `Object::STRING`, `Object::FLATTENED` e `Dynamic::FLATTENED` existem para compatibilidade do protocolo nativo com clientes que não implementam Object/Dynamic por completo. Eles não aparecem no armazenamento em disco do MergeTree.
* **`V3` existe apenas em disco.** `Object::V3` e `Dynamic::V3` constam da lista porque esses valores já foram atribuídos e não devem ser reutilizados, e não porque um decodificador possa encontrá-los: o ClickHouse nunca os envia via `Native` e, nesse formato, os rejeita com `INCORRECT_DATA`. Tudo o que eles carregam — a subversão de shared-data de `Object::V3` e seus layouts de bucket e de granule — descreve uma parte, e não um bloco, e por isso está fora do escopo desta página. Clientes que consomem o protocolo TCP nativo recebem `V1`, `V2`, `STRING` ou `FLATTENED`.

#### LowCardinality(T)

O tipo versionado mais simples. Ele substitui uma coluna de `N` valores internos por um pequeno dicionário de valores únicos, acompanhado de `N` índices que apontam para esse dicionário.

String do tipo: `LowCardinality(InnerType)`. Exemplos: `LowCardinality(String)`, `LowCardinality(FixedString(4))`, `LowCardinality(Nullable(String))`.

```text theme={null}
[per block with rows > 0]:
  [8 bytes:  Int64 LE state prefix = 1]             ← repeated at the start of every non-empty block
  [8 bytes:  UInt64 LE metadata]                    ← key type code (low byte) + flag bits
  [8 bytes:  UInt64 LE dict_size]                   ← number of dict entries (incl. placeholder slot)
  [N bytes:  dict values]                           ← inner type's encoding for dict_size values
  [8 bytes:  UInt64 LE keys_count]                  ← number of values at this recursive level (see below)
  [K bytes:  keys]                                  ← (1 << key_type_code) bytes per key
```

O prefixo de estado (Int64 LE = 1) é a única versão definida, `sharedDictionariesWithAdditionalKeys`; os demais valores são reservados.

O metadado UInt64 por bloco é um campo de bits:

| Faixa de bits | Significado |
| - | - |
| 0..7 | Código do tipo da chave: `0` = UInt8, `1` = UInt16, `2` = UInt32, `3` = UInt64. É escolhido o menor tipo capaz de indexar `dict_size` entradas. |
| 8 (`0x100`) | `NeedGlobalDictionaryBit` — um único dicionário compartilhado entre blocos. **Nunca é definido no formato `Native`**: o gravador Native usa `low_cardinality_max_dictionary_size = 0`, e o leitor Native rejeita esse bit (`native_format` lança `INCORRECT_DATA` — "cannot use global dictionary"). Ele pertence ao stream em disco do MergeTree, não ao wire. |
| 9 (`0x200`) | `HasAdditionalKeysBit` — definido quando o bloco contém chaves de dicionário adicionais (gravadas antes dos índices). Sempre definido em um bloco `Native` não vazio. |
| 10 (`0x400`) | `NeedUpdateDictionary` — definido quando o bloco contém uma atualização de dicionário. Sempre definido em um bloco `Native` não vazio, já que cada bloco envia seu próprio dicionário autossuficiente. |

Em uma resposta de consulta típica, com um único bloco de dados por coluna, o metadado é `0x600` (HasAdditionalKeys + NeedUpdateDictionary).

Os valores do dict são `dict_size` valores codificados com o tipo interno T. O dicionário reserva os slots iniciais para valores especiais: uma coluna não anulável reserva um (`dict[0]` contém o valor default do tipo interno, por exemplo `""` para `String`), e os valores distintos reais começam em `dict[1]`.

Em `LowCardinality(Nullable(T))`, o dict continua codificado como T simples (sem stream de mapa de nulos), mas **dois** slots são reservados: `dict[0]` é o marcador NULL e `dict[1]` é o valor default do tipo interno (por exemplo `""` para `String`); os valores distintos reais começam em `dict[2]`. A chave de uma linha NULL aponta para `dict[0]`, e esse slot é gravado no wire com os bytes do valor default do tipo interno.

As chaves são índices no dict; cada índice ocupa `1 << key_type_code` bytes (1, 2, 4 ou 8), e o valor `N` é reconstruído como `dict[keys[N]]`.

`keys_count` é o número de valores `LowCardinality` no **nível recursivo atual**, não necessariamente a contagem de linhas do bloco. Em uma coluna `LowCardinality` de nível superior, os dois coincidem. Mas, quando o `LowCardinality` está dentro de um tipo composto, a contagem corresponde ao número achatado de valores que o tipo composto repassa: em `Array(LowCardinality(String))` com três linhas contendo cinco elementos no total, `keys_count` é `5`, e não `3`; em `Map(K, LowCardinality(V))`, é o número total de pares, e assim por diante. Um decodificador deve obter `keys_count` deste campo em vez de presumir a contagem de linhas do bloco. Quando essa contagem achatada é zero — por exemplo, em um bloco cujos arrays estão todos vazios —, a fase de dados do `LowCardinality` não grava **absolutamente nada**: apenas o prefixo de estado (emitido na [fase de prefixo do tipo composto](#composite-types)) está presente, sem metadado, dicionário ou `keys_count` em seguida.

O prefixo de estado é lido no início de cada bloco cuja contagem de linhas é maior que zero — blocos de cabeçalho (rows = 0) e blocos vazios não emitem nada. Dentro de um bloco, `keys_count` é igual à contagem de linhas, `dict_size` é igual ao número de valores no stream do dict, e cada chave cabe em `1 << key_type_code` bytes.

> [!NOTE]
>
> No formato `Native`, cada bloco carrega um **dicionário autocontido e local ao bloco** — não existe estado de dicionário compartilhado entre blocos. O gravador Native define `low_cardinality_max_dictionary_size = 0`, de modo que `SerializationLowCardinality` nunca constrói um dicionário compartilhado: cada bloco não vazio grava suas chaves como chaves adicionais locais ao bloco, com `NeedGlobalDictionaryBit` desativado (metadado `0x600`), e o leitor Native rejeita `NeedGlobalDictionaryBit` quando `native_format` é true. Portanto, um decodificador deve redefinir o dicionário a cada bloco e ler as `dict_size` entradas presentes nesse bloco; reaproveitar o dicionário de um bloco anterior levaria a uma leitura incorreta das chaves do bloco seguinte. (Persistir um dicionário LC entre blocos é uma questão do armazenamento em disco do MergeTree, e não do layout wire do Native.)

`LowCardinality(String)` com os valores `['a', 'b', 'a', 'c', 'b']`:

```text theme={null}
01 00 00 00 00 00 00 00      state prefix Int64 = 1
00 06 00 00 00 00 00 00      metadata UInt64 = 0x600
04 00 00 00 00 00 00 00      dict_size = 4
00                           dict[0] = "" (placeholder)
01 'a'                       dict[1] = "a"
01 'b'                       dict[2] = "b"
01 'c'                       dict[3] = "c"
05 00 00 00 00 00 00 00      keys_count = 5
01 02 01 03 02               keys (UInt8): 1, 2, 1, 3, 2
```

Reconstruído: `dict[1], dict[2], dict[1], dict[3], dict[2]` = `["a", "b", "a", "c", "b"]`.

`LowCardinality(Nullable(String))` com os valores `['a', NULL, '', 'b']` mostra os dois slots reservados — `dict[0]` para NULL e `dict[1]` para o valor padrão de string vazia:

```text theme={null}
01 00 00 00 00 00 00 00      state prefix Int64 = 1
00 06 00 00 00 00 00 00      metadata UInt64 = 0x600
04 00 00 00 00 00 00 00      dict_size = 4
00                           dict[0] = "" → NULL marker
00                           dict[1] = "" → inner default value
01 'a'                       dict[2] = "a"
01 'b'                       dict[3] = "b"
04 00 00 00 00 00 00 00      keys_count = 4
02 00 01 03                  keys (UInt8): 2, 0, 1, 3
```

Reconstruído: `dict[2]` = `"a"`, `dict[0]` = `NULL`, `dict[1]` = `""`, `dict[3]` = `"b"`, ou seja, `["a", NULL, "", "b"]`. Tanto `dict[0]` quanto `dict[1]` são bytes vazios no wire; o valor nulo é determinado pela chave que aponta para o slot `0`, e não pelos bytes.

#### JSON (Tier 1: fallback String)

O tipo `JSON` do ClickHouse tem várias codificações wire (consulte [a referência de versões de serialização](#serialization-version-reference)). O Tier 1 é o mais simples: quando a configuração por consulta `output_format_native_write_json_as_string = 1` está definida, o servidor converte cada valor JSON em seu texto serializado e emite a coluna como `String`, com um marcador de prefixo de estado.

String do tipo: `JSON`.

```text theme={null}
[8 bytes:  Int64 LE state prefix = 1]        ← JSONStringSerializationVersion
[per block with rows > 0]:
  [N bytes: String column encoding for num_rows JSON text values]
```

O valor do prefixo de estado é `1` para este fallback String. Os demais valores indicam diferentes codificações de `JSON`/`Object`: `0` = V1, `2` = V2 (o padrão no protocolo TCP nativo), `3` = FLATTENED, `4` = V3 (consulte [a referência de versões de serialização](#serialization-version-reference)). Se o decodificador encontrar aqui um valor diferente de `1`, não se trata do fallback String. O prefixo é lido no início de cada bloco com linhas > 0, e o stream de valores é uma coluna [String](#string-type) padrão com `num_rows` linhas.

Valor `JSON` `'{"a":1}'` (uma linha):

```text theme={null}
01 00 00 00 00 00 00 00      state prefix Int64 = 1
07 7B 22 61 22 3A 31 7D      String: 7 bytes {"a":1}
```

O valor é emitido como texto JSON compacto — `{"a":1}`, com o inteiro preservado como inteiro. O texto é apenas um valor `String`; assim, o cliente recebe o JSON para trânsito opaco, mas não recupera os caminhos individuais nem seus tipos do ClickHouse. Para obter uma tipagem fiel por caminho, é necessária a codificação Tier 2 descrita abaixo.

#### Variant(T1, T2, ...)

Uma união discriminada: cada linha contém um valor de exatamente um dos tipos variantes, ou NULL. Cada linha carrega um **discriminador global** de um byte que seleciona seu tipo, e os valores de cada tipo são então armazenados de forma densa, em um bloco contíguo por tipo variante.

String do tipo: `Variant(T1, T2, ...)`. O servidor normaliza a ordem (os tipos variantes são ordenados por nome), de modo que a string do tipo, tal como é recebida, já lista os tipos na **ordem do discriminador global**: o discriminador `0` seleciona o primeiro tipo listado, `1` o segundo, e assim por diante. `255` (`NULL_DISCRIMINATOR`) indica que a linha é NULL. Os elementos de Variant nunca são `Nullable`, pois representar NULL é função do discriminador. Exemplos: `Variant(String, UInt64)`, `Variant(Array(UInt8), String)`.

O prefixo de estado carrega o modo de discriminante como `UInt64 LE`: `0` = BASIC (o discriminador de cada linha é gravado literalmente), `1` = COMPACT (codificação run-length por granule). Por padrão, o servidor usa BASIC no protocolo nativo (`use_compact_variant_discriminators_serialization = false`); apenas o BASIC é especificado aqui.

```text theme={null}
[per block with rows > 0]:
  [8 bytes:  UInt64 LE discriminators mode = 0]    ← state prefix, repeated at the start of every non-empty block;
                                                     followed by each variant element's own state prefix
                                                     (empty for leaf types)
  [num_rows bytes: UInt8 discriminators]           ← one global discriminator per row; 255 = NULL
  [for each variant type i, in declared order]:
    [values for the rows whose discriminator == i] ← dense encoding in type i; count = #rows selecting i
```

Para reconstruir, percorra os discriminadores da esquerda para a direita, mantendo um contador acumulado por tipo. A linha `r` com discriminador `d` (≠ 255) recebe o valor no índice `counter[d]` da sequência de valores do tipo variante `d` e, em seguida, `counter[d]` é incrementado. Linhas com discriminador `255` são NULL e não consomem valor de nenhuma sequência; assim, a soma dos contadores por tipo é igual ao número de linhas não NULL.

O prefixo de estado (o `UInt64` de modo) é lido no início de cada bloco com linhas > 0; blocos de cabeçalho e blocos vazios não emitem nada. Cada discriminador não NULL é menor que o número de tipos variantes, e o tipo variante `i` é decodificado para exatamente `count[i]` linhas.

> [!NOTE]
>
> Elementos de Variant que também são stateful (`LowCardinality`, `Variant`, `Dynamic`, `JSON`) emitem seu próprio prefixo de estado na fase de prefixo de estado por elemento, após o `UInt64` de modo. Tipos folha e os tipos compostos simples (`Array`, `Tuple`, `Map` de tipos folha) têm prefixos de estado vazios e podem ser combinados livremente.

`Variant(String, UInt64)` com os valores `[42, 'hi', NULL]` (a ordem canônica coloca `String` antes de `UInt64`; portanto, discriminador 0 = String, 1 = UInt64):

```text theme={null}
00 00 00 00 00 00 00 00      state prefix: UInt64 discriminators mode = 0 (BASIC)
01 00 FF                     discriminators (3 rows): 1 (UInt64), 0 (String), 255 (NULL)
02 68 69                     String run (1 value): len=2 "hi"
2A 00 00 00 00 00 00 00      UInt64 run (1 value): 42
```

Reconstruído: linha 0 = UInt64 run\[0] = `42`; linha 1 = String run\[0] = `"hi"`; linha 2 = NULL.

O stream de discriminadores atua como índice: cada discriminador não NULL extrai o próximo valor da sequência densa do seu tipo, enquanto `255` (NULL) não consome nada. Esse mesmo percurso reconstrói o [Dynamic](#dynamic), que difere apenas na forma como o NULL é codificado:

![Discriminadores selecionando valores de sequências densas específicas de cada tipo](https://mintcdn.com/private-7c7dfe99/kA9td9fgr8TRWXKS/images/interfaces/native-format/variant-discriminators.webp?fit=max\&auto=format\&n=kA9td9fgr8TRWXKS\&q=85\&s=b722e6e431e0afbfd7a719d4b9711ea2)

#### Dynamic

Uma coluna cujo tipo de valor é descoberto em tempo de execução: cada linha contém um valor de um dos tipos de um conjunto determinado em tempo de execução, ou NULL. Diferentemente de `Variant`, o conjunto de tipos **não** está na string do tipo da coluna — ele é transportado no prefixo de estado.

String do tipo: `Dynamic` ou `Dynamic(max_types=N)`. O parâmetro `max_types` limita quantos tipos distintos a coluna rastreia, mas não afeta o formato wire descrito abaixo.

`Dynamic` tem quatro codificações — `V1 = 1`, `V2 = 2`, `FLATTENED = 3`, `V3 = 4`. A codificação emitida pelo servidor depende do canal e das configurações da consulta:

* Via `clickhouse-client` e HTTP `FORMAT Native`, a revisão do componente de gravação é `0` (a menos que seja elevada com `client_protocol_version`); portanto, o padrão é **V1**.
* Via protocolo TCP nativo, na revisão negociada, o padrão é **V2**. O componente de gravação `Native` mantém as estatísticas desabilitadas, então uma carga útil `V2` padrão não contém estatísticas por variante — logo após a lista de tipos vêm o prefixo e os dados do `Variant` aninhado. (Estatísticas por variante dizem respeito ao armazenamento em disco do MergeTree e não fazem parte do formato wire Native.)
* A configuração de consulta `output_format_native_use_flattened_dynamic_and_json_serialization = 1` se sobrepõe a ambos os casos e emite **FLATTENED (versão 3)**, independentemente da revisão.

> [!NOTE]
>
> **Escopo**
>
> Esta página especifica apenas o layout **`FLATTENED`**. Os layouts binários não flat `V1`/`V2`/`V3` correspondem à representação interna/em disco (listas de tipos codificadas em binário, estatísticas por variante) e **não** são especificados aqui. Um cliente que queira decodificar `Dynamic` com base nesta página deve solicitar `FLATTENED` definindo `output_format_native_use_flattened_dynamic_and_json_serialization = 1`; o layout abaixo pressupõe essa configuração. Como o byte de versão fica no início do prefixo, um decodificador consegue identificar a codificação efetivamente recebida e rejeitar `V1`/`V2`/`V3` caso implemente apenas `FLATTENED`.

O layout **FLATTENED (versão 3)** selecionado por essa configuração:

```text theme={null}
[per block with rows > 0]:
  [8 bytes:  UInt64 LE version = 3]                ← state prefix, repeated at the start of every non-empty block
  [VarUInt num_types]                              ← number of runtime types
  [num_types × type]                               ← type names, in wire order; each a String, or a binary
                                                     type encoding when output_format_native_encode_types_in_binary_format = 1,
                                                     except for a secondary query (see the type field note)
  [per type: its own state prefix]                 ← empty for leaf types; + indexes-type prefix (empty, integer)
  [num_rows × discriminator]                       ← width by num_types (UInt8 if ≤ 255, else UInt16/32/64);
                                                     NULL discriminator = num_types (one past the last type)
  [for each type i, in wire order]:
    [values for the rows whose discriminator == i] ← dense encoding in type i
```

A largura do discriminador é o menor inteiro sem sinal capaz de indexar `num_types` tipos mais o slot de NULL — `UInt8` para `num_types ≤ 255`, depois `UInt16`, `UInt32`, `UInt64`. NULL corresponde ao próprio valor de discriminador `num_types`, diferentemente de `Variant`, em que NULL é o valor fixo `255`. A reconstrução segue o mesmo percurso denso de `Variant`: mantenha um contador por tipo; a linha `r` com discriminador `d` (≠ `num_types`) recebe o valor `counter[d]` da sequência do tipo `d`.

O prefixo de estado (versão + lista de tipos) é lido no início de cada bloco com linhas > 0; blocos de cabeçalho e blocos vazios não emitem nada.

> [!NOTE]
>
> **Contagens malformadas**
>
> `num_types` é lido do stream antes de qualquer nome de tipo. Um decodificador deve tratá-lo como não confiável e não deve usá-lo diretamente para dimensionar uma alocação — nem uma contagem próxima de `SIZE_MAX` (que pode causar overflow na aritmética intermediária ou lançar uma exceção que não seja `DB::Exception`), nem uma contagem grande, porém representável, como `100000000` (que está muito abaixo do `max_size()` de um vetor, mas alocaria gigabytes antes mesmo de um único nome de tipo ser lido). O ClickHouse lê a lista de tipos uma entrada por vez, usando apenas uma dica de pré-alocação limitada, de modo que um `num_types` corrompido é rejeitado como `INCORRECT_DATA` ("`Dynamic` column has too many types", quando a contagem excede o que o contêiner comporta) ou como um erro de leitura comum quando o stream fica sem entradas de tipo — nunca como uma falha por falta de memória.
>
> **Não** limite, porém, o `num_types` **achatado** por `ColumnDynamic::MAX_DYNAMIC_TYPES_LIMIT` (`254` no ClickHouse): a lista de tipos achatada contém *todos* os tipos de tempo de execução distintos, incluindo aqueles que transbordaram para a variante compartilhada, de modo que um bloco achatado válido pode legitimamente listar muito mais do que isso. O limite `MAX_DYNAMIC_TYPES_LIMIT` se aplica apenas à contagem `num_dynamic_types` nos prefixos não flat `V1`/`V2`/`V3`, que conta os slots de variantes regulares e é limitada por esse valor (o ClickHouse a valida nesse ponto, antes do `+ 1` referente à variante compartilhada).

> [!NOTE]
>
> **Streams truncados**
>
> A sequência de discriminadores contém exatamente `num_rows` entradas, e um decodificador deve verificar se leu essa quantidade, em vez de aceitar o que quer que o stream contenha. Uma sequência mais curta não entra em contradição com nada do que vem depois dela: as contagens de valores por tipo são derivadas dos discriminadores efetivamente lidos, portanto permanecem consistentes com as sequências de valores, e uma sequência composta apenas de marcadores NULL não exige nenhum byte de valor. O ClickHouse rejeita uma sequência de discriminadores curta com `INCORRECT_DATA` ("Mismatch in flattened `Dynamic` column: N rows are expected, but the indexes stream contains only M rows") antes de qualquer verificação por tipo, porque a coluna reconstruída passa então a ser usada para `num_rows` linhas, independentemente de seu tamanho real.

> [!NOTE]
>
> Tipos de tempo de execução cuja serialização é stateful (`LowCardinality`, `Variant`, `Dynamic`, `JSON`) carregam prefixos de estado aninhados após a lista de nomes de tipo.

A lista de tipos de tempo de execução normalmente segue a canonicalização de `Variant` — os slots de variantes regulares são gravados na ordem de `DataTypeVariant` (por nome de tipo), de modo que a ordem no fluxo binário não corresponde à ordem de inserção. No entanto, ela **nem sempre** está ordenada globalmente: tipos que transbordaram para a variante compartilhada (por exemplo, com `Dynamic(max_types=N)`) são anexados após os slots regulares na ordem em que apareceram pela primeira vez, então o final da lista pode quebrar a ordem por nome de tipo. Portanto, um decodificador deve considerar a lista de tipos transmitida como a referência definitiva para a atribuição de discriminadores e não deve reordená-la por conta própria. O que a lista não pode conter é um tipo repetido ou `Nothing`: duas entradas iguais são mapeadas para um único discriminador, e `Nothing` nem sequer é uma variante; em ambos os casos, portanto, parte da lista não tem onde ser reconstruída. O ClickHouse rejeita os dois casos com `INCORRECT_DATA`. Para as linhas `[42::UInt64, "hi", NULL]`, os dois tipos são `String` e `UInt64`, e `"String"` vem antes de `"UInt64"` na ordenação, então os discriminadores são `0` = String, `1` = UInt64, `2` = NULL:

```text theme={null}
03 00 00 00 00 00 00 00      state prefix: UInt64 version = 3 (FLATTENED)
02                           VarUInt num_types = 2
06 53 74 72 69 6E 67         type[0] = "String"
06 55 49 6E 74 36 34         type[1] = "UInt64"
01 00 02                     discriminators (3 rows): 1 (UInt64), 0 (String), 2 (NULL)
02 68 69                     String run (type[0], 1 value): len=2 "hi"
2A 00 00 00 00 00 00 00      UInt64 run (type[1], 1 value): 42
```

Reconstruído: linha 0 = UInt64 run\[0] = `42`; linha 1 = String run\[0] = `"hi"`; linha 2 = NULL. Os runs de cada tipo seguem a mesma ordem de wire da lista de tipos (`String` antes de `UInt64`).

#### JSON (Tier 2: FLATTENED Object)

A codificação JSON mais rica: em vez de converter cada valor em texto (Tier 1), a coluna é dividida em uma subcoluna por caminho JSON. Ela é selecionada quando **não** se solicita o fallback do Tier 1 (`output_format_native_write_json_as_string = 0`) e a flag de serialização achatada está ativada (`output_format_native_use_flattened_dynamic_and_json_serialization = 1`); nesse caso, o servidor emite a serialização **versão 3**.

Há dois tipos de caminho:

* **Caminhos tipados** são declarados na string do tipo, por exemplo `JSON(a UInt32, b String)`, e decodificados no tipo declarado. Nomes de caminho que contêm pontos são delimitados por backticks na string do tipo.
* **Caminhos dinâmicos** são descobertos em tempo de execução, e cada um é decodificado como uma coluna [Dynamic](#dynamic).

No modo FLATTENED, **não há coluna shared-data** (esse armazenamento de excedentes pertence às codificações Object V2/V3 não flat). Cada caminho é uma coluna completa com `num_rows` valores.

```text theme={null}
[per block with rows > 0]:
  -- prefix phase (repeated at the start of every non-empty block):
  [8 bytes:  UInt64 LE version = 3]                ← state prefix
  [VarUInt num_dynamic_paths]
  [num_dynamic_paths × String]                     ← dynamic path names, in wire order
  [per typed path: its column's state prefix]      ← empty for leaf types
  [per dynamic path: a Dynamic state prefix]       ← version + type list (see Dynamic)
  -- data phase:
  [for each typed path:   its column's data]       ← num_rows values in the declared type
  [for each dynamic path: its Dynamic data]        ← num_rows values (discriminators + runs)
```

Observe a estrutura em duas fases: **todos** os prefixos de estado dos caminhos vêm primeiro e, depois, **todos** os dados dos caminhos. Por isso, o prefixo `Dynamic` de um caminho dinâmico (na fase de prefixo) fica separado de seus dados (na fase de dados). O prefixo de estado é lido no início de cada bloco com linhas > 0, e cada coluna de caminho (tipado ou dinâmico) contém exatamente `num_rows` valores. O objeto da linha `r` é montado lendo o valor de cada caminho no índice `r`; um caminho dinâmico cujo discriminador `Dynamic` seja NULL nessa linha não contribui com nenhuma chave. Os nomes dos caminhos dinâmicos devem ser distintos, e nenhum deles pode repetir um caminho que a string do tipo já declara como tipado — em qualquer um dos casos, o objeto montado teria dois valores sob a mesma chave; por isso, o ClickHouse rejeita ambos com `INCORRECT_DATA`. O mesmo vale para a lista de caminhos dinâmicos das codificações não flat.

> [!NOTE]
>
> **Contagens malformadas**
>
> `num_dynamic_paths` no layout FLATTENED documentado aqui — e a contagem de caminhos dinâmicos nas codificações não flat `V1`/`V2` — é lido do stream antes dos nomes dos caminhos. (Não há um campo separado de caminhos achatados nos prefixos não flat: eles trazem apenas a contagem de caminhos dinâmicos, além de um valor `max_dynamic_paths` em `V1` que é lido e descartado.) Assim como em [`Dynamic`](#dynamic), um decodificador deve tratar essas contagens como não confiáveis e não deve dimensionar uma alocação diretamente a partir delas — nem uma contagem da família `SIZE_MAX`, nem uma contagem grande, porém representável. O ClickHouse lê os nomes dos caminhos uma entrada por vez, usando apenas uma dica de pré-alocação limitada; assim, uma contagem corrompida é rejeitada com `INCORRECT_DATA` ("JSON/Object column has too many paths", quando excede o que o contêiner comporta) ou como um erro de leitura comum quando os nomes de caminhos no stream se esgotam.

> [!NOTE]
>
> **Streams truncados**
>
> Os `num_rows` valores por coluna de caminho são um requisito obrigatório, não um limite superior: um decodificador deve verificar cada coluna de caminho, tipado e dinâmico, com base na contagem de linhas do bloco, em vez de confiar que o stream terminará onde deveria. O objeto montado não tem um tamanho independente para comparação — sua contagem de linhas é simplesmente o resultado da reconstrução —, portanto uma coluna de caminho incompleta não pode ser detectada depois, e ler a linha `r` dela ultrapassa seu final. O ClickHouse a rejeita com `INCORRECT_DATA` ("Unexpected size of typed path / flattened path *p* in flattened Object column"). No caso de um caminho dinâmico, isso se soma à verificação equivalente dentro de sua coluna [`Dynamic`](#dynamic).

Valor `JSON` `{"a": 42, "b": "hi"}` (uma linha, ambos os caminhos dinâmicos). Um inteiro JSON é inferido como `Int64`:

```text theme={null}
03 00 00 00 00 00 00 00      version = 3 (Object)
02                           num_dynamic_paths = 2
01 61                        path "a"
01 62                        path "b"
03 00 00 00 00 00 00 00 01 05 49 6E 74 36 34      "a" Dynamic prefix: version 3, 1 type, "Int64"
03 00 00 00 00 00 00 00 01 06 53 74 72 69 6E 67   "b" Dynamic prefix: version 3, 1 type, "String"
00 2A 00 00 00 00 00 00 00   "a" data: discriminator 0, Int64 42
00 02 68 69                  "b" data: discriminator 0, String "hi"
```

#### JSON não flat (V1/V2)

As codificações não achatadas de `Object` são o que o servidor emite no wire quando a flag de achatamento está desativada: `V1` via `clickhouse-client` / HTTP `FORMAT Native` (revisão `0`) e `V2` via protocolo TCP nativo. (`V3`, a terceira codificação não flat, é gravada apenas em partes MergeTree e é rejeitada no wire.) Essas codificações contêm uma coluna de dados compartilhados (shared-data) e **não** são especificadas nesta página. Observe que elas **não** contêm estatísticas por caminho no wire Native: o `NativeWriter` mantém as estatísticas desabilitadas, portanto o prefixo de estrutura de `Object` não tem seção de estatísticas, e os bytes que vêm depois dele já são diretamente os prefixos e os dados typed/dynamic/shared-data. As estatísticas aparecem apenas nos caminhos em disco do MergeTree que as habilitam. Para decodificar uma coluna `JSON` com base nesta página, o cliente deve escolher um dos tiers documentados: definir `output_format_native_write_json_as_string = 1` para o [fallback String](#json-tier-1-string-fallback) ou `output_format_native_use_flattened_dynamic_and_json_serialization = 1` (com `output_format_native_write_json_as_string = 0`) para o layout [FLATTENED Object](#json-tier-2-flattened-object).

## Frame de compressão

O ClickHouse pode comprimir os dados de coluna de um stream `Native` usando um formato de frame interno. O [layout do frame](#frame-format) abaixo é **independente do transporte** — os mesmos frames aparecem tanto no protocolo TCP nativo quanto no HTTP —, mas a forma de solicitar a compressão, e o que envolve os frames, varia conforme o transporte.

* **Protocolo TCP nativo.** A compressão é opcional e ativada por consulta, por meio da flag `compression` no [pacote Query](https://clickhouse.com/docs/pt-BR/resources/develop-contribute/native-protocol/protocol.md#query). Quando ativa, o corpo de cada pacote `Data`, `Totals`, `Extremes`, `Log` e `ProfileEvents` — os bytes após a string `table_name` — é encapsulado no formato de frame. O envelope do pacote em si, ou seja, o código do tipo de pacote e a string `table_name`, **não** é comprimido; o servidor os grava no stream bruto. Tudo o que o `NativeWriter` emite vai para o stream comprimido, de modo que o prefixo `BlockInfo` é a primeira coisa dentro do frame, junto com as dimensões e as colunas. Portanto, o cliente precisa descomprimir o frame antes de ler o `BlockInfo`.
* **HTTP.** `SELECT ... FORMAT Native&compress=1` encapsula todo o stream de bytes do `FORMAT Native` nos mesmos frames (o servidor usa o mesmo `CompressedWriteBuffer` interno), e `?decompress=1` espera os mesmos frames em um corpo de *entrada* `Native`, decodificando-os por meio do `CompressedReadBuffer` correspondente. Nesse caminho, não há tipo de pacote TCP, `table_name` nem envelope de pacote: toda a carga útil comprimida consiste apenas em blocos `Native` enquadrados (o prefixo `BlockInfo` só está presente se a revisão negociada for maior que `0`, exatamente como no layout não comprimido acima). Esse enquadramento interno de `compress`/`decompress` não se confunde com a compressão de transporte HTTP (`Content-Encoding: gzip`/`zstd`, habilitada por `enable_http_compression`), que encapsula a resposta na camada HTTP e não corresponde ao formato de frame abaixo.

Assim, um cliente que tenha implementado apenas o layout não comprimido do `FORMAT Native` ainda precisa adicionar essa camada de frames para ler uma resposta HTTP `Native` comprimida ou para enviar um corpo da requisição com `decompress=1`.

### Formato do frame

```text theme={null}
[16 bytes: CityHash128 checksum over the 9-byte header + compressed body]
[1 byte:   method]                 ← 0x82 = LZ4, 0x90 = ZSTD, 0x02 = NONE
[4 bytes:  compressed_size LE u32] ← INCLUDES the 9-byte header, EXCLUDES the 16-byte checksum
[4 bytes:  uncompressed_size LE u32]
[N bytes:  compressed body]        ← N = compressed_size - 9
```

O tamanho total do frame é `16 + compressed_size` = `16 + 9 + body_size` = `25 + body_size`. Observe os dois intervalos: o checksum cobre o cabeçalho de 9 bytes mais o corpo, enquanto `compressed_size` contabiliza o cabeçalho e o corpo, mas **não** o próprio checksum:

![Campos do frame de compressão e fronteira do tamanho comprimido](https://mintcdn.com/private-7c7dfe99/kA9td9fgr8TRWXKS/images/interfaces/native-format/compression-frame.webp?fit=max\&auto=format\&n=kA9td9fgr8TRWXKS\&q=85\&s=1d0d4d1a7615bd7bf38eee8504fc7296)

### Valores do byte de método

Estes três codecs são os que o servidor produz para o enquadramento `Native` de stream completo: a saída HTTP com `compress=1` usa o codec padrão do servidor (`ZSTD(3)`), e o protocolo TCP nativo usa `LZ4`, `ZSTD` ou `NONE`, conforme o valor de `network_compression_method`. Um cliente `Native` genérico só precisa produzir e consumir esses três.

| Byte | Método | Codificação do corpo |
| - | - | - |
| `0x02` | NONE | O corpo contém os bytes brutos (sem compressão). O frame é emitido mesmo assim; o receptor verifica o checksum. |
| `0x82` | LZ4 | O corpo está no **formato de bloco LZ4** — *não* no formato de frame LZ4. Não há número mágico. |
| `0x90` | ZSTD | O corpo é um stream zstd bruto de frame único (o número mágico padrão do zstd faz parte do corpo). |

O byte de método também codifica os [codecs em nível de coluna](https://clickhouse.com/docs/pt-BR/reference/statements/create/table/codec.md). Eles são aplicados por coluna nos caminhos em disco do MergeTree, e não ao enquadramento de stream completo, mas o caminho de entrada HTTP com `decompress=1` obtém o codec a partir do byte de método de cada frame; por isso, qualquer um desses bytes pode aparecer legitimamente na entrada. Assim, um decodificador em conformidade deve reconhecer todo o espaço de valores atribuídos e rejeitar os bytes que não implementa, em vez de interpretar o corpo de forma incorreta. Os corpos desses codecs têm formato específico de cada um e estão fora deste contrato genérico de frame:

| Byte | Método |
| - | - |
| `0x91` | `Multiple` (um codec composto que encapsula uma sequência de codecs aninhados) |
| `0x92` | `Delta` |
| `0x93` | `T64` |
| `0x94` | `DoubleDelta` |
| `0x95` | `Gorilla` |
| `0x96` | `AES_128_GCM_SIV` (criptografia) |
| `0x97` | `AES_256_GCM_SIV` (criptografia) |
| `0x98` | `FPC` |
| `0x9a` | `GCD` |
| `0x9c` | `ALP` |
| `0x9d` | `SZ3` |
| `0x9e` | `Quantized` |
| `0x9f` | `ZXC` |

`DoubleDelta` (`0x94`) armazena diferenças de segunda ordem como bits de prefixo/sinal/magnitude, com o MSB primeiro. A decodificação baseada em palavras preserva essa codificação, a contagem de elementos e o preenchimento do byte final.

`0x9c` (`ALP`) é um codec sem perdas em **beta** para `Float32` e `Float64` (compressão adaptativa de ponto flutuante sem perdas). Só é possível criar uma tabela com `CODEC(ALP)` quando a configuração `enable_alp_codec` está habilitada, mas o byte de método é sempre aceito na descompressão, para que dados gravados anteriormente continuem legíveis. `0x9d` (`SZ3`) é um codec *com perdas* **experimental**, com erro limitado, para `Float32`, `Float64` e `Array` desses tipos. Só é possível criar uma tabela com `CODEC(SZ3)` quando a configuração `enable_sz3_codec` está habilitada, mas o byte de método é sempre aceito na descompressão, para que dados gravados anteriormente continuem legíveis. `0x9f` (`ZXC`) é um codec LZ assimétrico **experimental**: lento na compressão, mas muito rápido na descompressão, com taxa de compressão entre a do `LZ4` e a do `ZSTD`. Só é possível criar uma tabela com `CODEC(ZXC)` quando a configuração `enable_zxc_codec` está habilitada, mas o byte de método é sempre aceito na descompressão, para que dados gravados anteriormente continuem legíveis. Os bytes `0x99` (`DeflateQpl`) e `0x9b` (`ZSTD_QPL`) foram atribuídos a codecs que já foram removidos; eles permanecem reservados e não são reutilizados.

`0x9e` (`Quantized`) é um codec de coluna **experimental** para colunas vetoriais densas (`Array(Float32)` e tipos semelhantes). Assim como o `NONE`, ele apenas repassa os dados — o corpo em precisão total é armazenado sem alterações —, mas sua presença anexa uma serialização que grava um stream complementar, compacto e quantizado, usado para acelerar a busca vetorial. Só é possível criar uma tabela com `CODEC(Quantized(...))` quando a configuração `enable_quantized_codec` está habilitada, e o byte de método é sempre aceito na descompressão.

### Checksum

O ClickHouse usa o CityHash v1.0.2 (a variante histórica), **e não** o CityHash moderno do Google; os dois produzem saídas diferentes.

O checksum é calculado sobre os 9 bytes do cabeçalho (method + compressed\_size + uncompressed\_size) mais os N bytes do corpo, ou seja, tudo o que está entre o checksum e o fim do frame. Os primeiros 8 bytes da saída de 16 bytes do CityHash128 correspondem à metade inferior (LE), e os 8 bytes seguintes, à metade superior (LE). Um decodificador recalcula o CityHash128 sobre o cabeçalho e o corpo recebidos e compara o resultado com os 16 bytes iniciais; qualquer divergência indica corrupção, e o decodificador falha.

### Fronteiras por bloco

A carga útil comprimida de um bloco é um **stream de um ou mais frames**, não necessariamente um único frame. O remetente grava o bloco serializado por meio de um `CompressedWriteBuffer`, que emite um frame sempre que seu buffer interno fica cheio (≈1 MB, `DBMS_DEFAULT_BUFFER_SIZE`) e um frame final quando é feito o flush do bloco. Assim, um bloco pequeno corresponde a um único frame; um bloco grande, a vários frames consecutivos.

O invariante vale em apenas um sentido: como o remetente faz o flush do buffer comprimido ao final de cada bloco, **o fim de cada bloco coincide com uma fronteira de frame**, mas o inverso não é verdadeiro. Uma fronteira de frame intermediária, emitida quando o buffer ficou cheio no meio de um bloco, cai no *meio* desse bloco e não é uma fronteira de bloco. Portanto, um decodificador deve usar as próprias dimensões do bloco (`num_columns`/`num_rows`) para determinar onde ele termina; não deve presumir que cada frame seja um bloco completo.

Um receptor processa os frames em stream: lê 16 + 9 bytes, lê exatamente `compressed_size - 9` bytes do corpo, descomprime para exatamente `uncompressed_size` bytes e repassa esses bytes ao decodificador de blocos; quando o decodificador precisa de mais dados do que o frame atual contém, lê o próximo frame. Como o remetente faz o flush a cada bloco, depois que um bloco é totalmente decodificado, o buffer de frame fica vazio e o próximo bloco começa em um novo frame.

No protocolo TCP nativo, o envelope do pacote (o VarUInt do tipo de pacote e a string `table_name`) é gravado no stream **raw**, fora da carga útil comprimida; apenas o corpo do bloco (BlockInfo + colunas) é encapsulado em frames. O caminho HTTP `compress`/`decompress` não tem esse envelope: o stream inteiro é composto de blocos encapsulados em frames.

### Negociação

No protocolo TCP nativo, a compressão é definida por consulta, e não por conexão. O campo `compression: bool` do pacote Query a solicita apenas para aquela consulta. O servidor atende à requisição e emite corpos `Data`/`Totals`/`Extremes`/`Log`/`ProfileEvents` comprimidos durante todo o ciclo de vida da consulta (`Log`/`ProfileEvents` somente a partir da v54481). Ele também espera que os blocos Data *enviados* pelo cliente — tabelas externas, o marcador vazio de fim de dados e as linhas de INSERT — usem o mesmo enquadramento. Consultas subsequentes na mesma conexão podem se comportar de forma diferente.

No HTTP não existe pacote Query: o parâmetro de consulta `compress=1` seleciona a saída enquadrada para aquela requisição, e `decompress=1` indica que o corpo da requisição está enquadrado. A saída de `compress=1` é gravada com o codec padrão do servidor (`ZSTD(3)`), e não com `network_compression_method`; o leitor de `decompress=1` obtém o codec a partir do byte de método de cada frame, portanto qualquer codec é aceito na entrada.

> [!NOTE]
>
> Com a compressão ativada, o servidor também pode encaminhar colunas pelo caminho de serialização paralela de blocos / `ColumnBLOB` (`PARALLEL_BLOCK_MARSHALLING`, v54478) para blocos com mais de uma linha. Uma implementação que comprime dados de INSERT precisa estar preparada para lidar com esse caminho (ou desativá-lo explicitamente), para evitar a dessincronização do stream.

## Glossário

**Bloco** — a unidade de troca de dados no formato Native. Um bloco autodescritivo de linhas armazenadas em formato colunar. Consulte [estrutura de bloco e coluna](#block-and-column-structure).

**BlockInfo** — o cabeçalho de metadados que precede um bloco no caminho de pacotes Data via TCP (gravado sempre que a revisão da conexão for maior que zero). É uma sequência de campos condicionados à revisão e identificados por ID de campo. É omitido pelo formato de saída `Native`, que serializa na revisão `0`. Consulte [BlockInfo](#blockinfo).

**Corpo da coluna** — os bytes de uma coluna que contêm os valores propriamente ditos, após o cabeçalho da coluna (nome, tipo, byte has\_custom\_serialization). O layout é específico de cada tipo. Consulte [layout de coluna no wire](#column-wire-layout).

**Tipo composto** — um tipo construído a partir de um ou mais tipos internos, codificado como múltiplos streams por coluna. O formato no wire é estável e não versionado. Consulte [tipos compostos](#composite-types).

**Dicionário (LowCardinality)** — o array de valores únicos que uma coluna `LowCardinality(T)` referencia por meio de índices inteiros. Consulte [LowCardinality](#lowcardinality).

**Bloco vazio** — um bloco com `num_columns = 0` e `num_rows = 0`. É usado como sentinela: um marcador de fim de entrada no lado do cliente e um marcador de limite de stream no lado do servidor. Consulte [variantes de bloco](#block-variants).

**Bloco de cabeçalho** — um bloco com `num_columns > 0` e `num_rows = 0`, enviado pelo servidor como o primeiro pacote Data da resposta a uma consulta. Anuncia o schema do resultado. Consulte [variantes de bloco](#block-variants).

**Tipo interno** — o tipo encapsulado por um tipo composto. O tipo interno de `Array(UInt32)` é `UInt32`; o de `Nullable(T)` é `T`.

**Stream de offsets** — o array UInt64 de posições finais cumulativas que `Array`, `Map` e `Nested` usam para delimitar os elementos de cada linha. Consulte [Array](#array).

**Valor de preenchimento** — os bytes gravados nas posições nulas do stream de valores de uma coluna `Nullable(T)`. O decodificador os lê para avançar no stream, mas ignora seu conteúdo. Consulte [Nullable](#nullable).

**Bloco de resultado** — um bloco com `num_rows > 0` que transporta linhas reais do resultado da consulta. Consulte [variantes de bloco](#block-variants).

**Bloco de esquema** — sinônimo de bloco de cabeçalho, usado ao descrever a fase de INSERT, na qual o bloco de esquema informa ao cliente a estrutura de colunas esperada.

**Versão de serialização** — um número de versão no wire, definido por tipo, que os tipos versionados usam para declarar qual variante da codificação vem a seguir. Não deve ser confundida com a versão do protocolo. Consulte [versão de serialização: conceito](#serialization-version-concept).

**Prefixo de estado** — os bytes que precedem a carga útil de cada bloco em um tipo versionado. Contém a versão de serialização e (no caso de LowCardinality) os metadados do dicionário de cada bloco. É emitido no início de cada bloco com rows > 0 e não é mantido entre blocos.

**Stream** — uma sequência contígua de bytes dentro do corpo de uma coluna, que codifica um subcomponente lógico (um null-map, um array de offsets, um stream de valores). Tipos com múltiplos streams concatenam dois ou mais streams por coluna.
