> ## 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.

> Especificación del formato columnar Native de ClickHouse: primitivas wire, la estructura de Block y Column, la codificación de cada tipo de datos y el marco de compresión

# Formato Native

El formato Native es el formato wire columnar que ClickHouse utiliza para transferir datos tabulares. Se emplea en varios contextos:

* el cuerpo de los paquetes `Data`, `Totals`, `Extremes`, `Log` y `ProfileEvents` en el [protocolo TCP nativo](https://clickhouse.com/docs/es/resources/develop-contribute/native-protocol/protocol.md) (el paquete `TableColumns` **no** es un bloque Native: transporta dos cadenas binarias, por lo que su disposición se describe en la [especificación del protocolo nativo](https://clickhouse.com/docs/es/resources/develop-contribute/native-protocol/protocol.md));
* la salida de `SELECT ... FORMAT Native` a través de HTTP;
* las exportaciones a archivo generadas con `INTO OUTFILE ... FORMAT Native`;
* las cargas útiles de replicación entre servidores.

Esta página describe los bytes que componen un Block (la carga útil columnar) y las codificaciones de cada tipo por columna que lo forman. El enmarcado de paquetes, el estado de la conexión y la negociación de versión se tratan en la [especificación del protocolo nativo](https://clickhouse.com/docs/es/resources/develop-contribute/native-protocol/protocol.md).

Todos los campos enteros de varios bytes usan el orden little-endian. Los enteros con signo se representan en complemento a dos.

> [!TIP]
>
> Para una introducción al formato `Native` orientada al usuario (con ejemplos de `curl`), consulte la [página del formato Native](https://clickhouse.com/docs/es/reference/formats/Native.md). Esta especificación es la referencia de bajo nivel del formato wire.

## Descripción general

Todo lo que transporta filas en formato wire es un **Block**: un fragmento autodescriptivo de filas almacenado columna por columna. Primero van todos los valores de la columna 1, luego todos los de la columna 2, y así sucesivamente. Un Block contiene solo las columnas a las que hace referencia la consulta, nunca la tabla completa.

El `data` de una columna se organiza según la *familia* a la que pertenece su tipo. Las familias, en orden creciente de complejidad para el decodificador, son:

![Estructura de un bloque columnar en formato wire y sus familias de organización de datos](https://mintcdn.com/private-7c7dfe99/kA9td9fgr8TRWXKS/images/interfaces/native-format/block-structure.webp?fit=max\&auto=format\&n=kA9td9fgr8TRWXKS\&q=85\&s=e4e067d9d428c13b9fce7fa7fe812828)

* Los tipos de **ancho fijo** organizan `data` como `bytes_per_value × num_rows` bytes sin procesar, sin delimitación por fila.
* Los tipos **compuestos** (`Nullable`, `Array`, `Tuple`, `Map`, `Nested`) tienen una estructura recursiva que se deriva íntegramente de la cadena de tipo, sin prefijo de versión ni estado entre bloques.
* Los tipos **versionados / con estado** (`LowCardinality`, `JSON`, `Variant`, `Dynamic`) comienzan cada bloque no vacío con un prefijo de versión de serialización/estado. En el formato wire `Native`, este prefijo y cualquier diccionario son **por bloque**: el formato no conserva estado *entre* bloques (el escritor crea un estado de serialización nuevo para cada bloque y establece `low_cardinality_max_dictionary_size = 0`). El estado entre bloques atañe al almacenamiento en disco de MergeTree, no a la disposición en formato wire de Native.

## Primitivas de wire

El formato Native se basa en cuatro codificaciones primitivas.

| Primitiva | Tamaño | Descripción |
| - | - | - |
| VarUInt | 1–10 B | Entero sin signo de longitud variable LEB-128 |
| Entero de ancho fijo | 1, 2, 4, 8, 16, 32 B | Little-endian; complemento a dos para los tipos con signo |
| String | variable | Prefijo de longitud VarUInt + bytes sin procesar por valor; a partir de la revisión `54492`, un [stream de desplazamientos](#string-type) independiente |
| Bool | 1 B | `0x00` = false, distinto de cero = true |

### VarUInt

Un entero sin signo de longitud variable que utiliza la codificación LEB-128. Cada byte contiene 7 bits de datos en las posiciones 0–6 y 1 bit de continuación en la posición 7. El bit de continuación vale `1` cuando hay más bytes a continuación y `0` en el último byte.

| Rango de valores | Bytes |
| - | - |
| 0 – 127 | 1 |
| 128 – 16383 | 2 |
| 16384 – 2097151 | 3 |
| hasta el rango completo de UInt64 | hasta 10 |

Codificación del valor `300`:

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

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

Decodificación de los 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
```

### Enteros de ancho fijo

| Tipo | Bytes | Codificación |
| - | - | - |
| UInt8 | 1 | Byte sin procesar |
| UInt16 | 2 | Little-endian |
| UInt32 | 4 | Little-endian |
| UInt64 | 8 | Little-endian |
| UInt128 | 16 | Little-endian |
| UInt256 | 32 | Little-endian |
| Int8 | 1 | Byte sin procesar, complemento a dos |
| Int16 | 2 | Little-endian, complemento a dos |
| Int32 | 4 | Little-endian, complemento a dos |
| Int64 | 8 | Little-endian, complemento a dos |
| Int128 | 16 | Little-endian, complemento a dos |
| Int256 | 32 | Little-endian, complemento a dos |
| Float32 | 4 | IEEE 754 de precisión simple, little-endian |
| Float64 | 8 | IEEE 754 de precisión doble, little-endian |

Por ejemplo, el valor UInt32 `1` se codifica como `01 00 00 00` y el valor Int32 `-1`, como `FF FF FF FF`.

### String

Una secuencia de bytes precedida de su longitud:

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

No es necesario que la secuencia de bytes sea UTF-8 válido. Una cadena vacía se codifica como un único byte `0x00`, y las cadenas pueden contener cualquier valor de byte, incluidos caracteres NUL intermedios. La cadena `"ab"` se codifica como `02 61 62`; para decodificarla, lea la longitud VarUInt (`2`) y, a continuación, lea ese número de bytes.

### Bool

Un único byte. `0x00` es false; cualquier valor distinto de cero es true (el valor canónico es `0x01`).

## Estructura de bloques y columnas

### Disposición en formato wire del bloque

```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
```

La presencia del prefijo `BlockInfo` depende del canal, porque el escritor está parametrizado por una *revisión* (consulte [Revisión del protocolo y el formato Native](#protocol-revision) para ver el tema en detalle, incluido el hecho de que `client_protocol_version` solo afecta a la salida):

* En el **protocolo TCP nativo**, el servidor escribe los bloques con la revisión negociada de la conexión (un valor elevado: `DBMS_TCP_PROTOCOL_VERSION`, consulte `src/Core/ProtocolDefines.h`). `BlockInfo` se escribe siempre que esa revisión sea mayor que cero, lo que siempre ocurre en una conexión real. El byte `has_custom_serialization` de cada columna (consulte [disposición en formato wire de columnas](#column-wire-layout)) se escribe a partir de la revisión `54454`.
* El *formato de salida* `Native` (`SELECT ... FORMAT Native` sobre HTTP, `INTO OUTFILE ... FORMAT Native` y el formato `Native` que genera `clickhouse-client`) serializa con la revisión `0` *de forma predeterminada*. Con la revisión `0` se omiten tanto el prefijo `BlockInfo` como el byte `has_custom_serialization`, por lo que un bloque consta únicamente de `num_columns`, `num_rows` y las columnas.

  Sobre HTTP, esta revisión no es fija: un Client puede elevarla con el parámetro de consulta `?client_protocol_version=<n>`, y el servidor utiliza ese valor como revisión de serialización de la respuesta.

  Con un valor suficientemente alto, la salida HTTP incluye el prefijo `BlockInfo` (que se escribe siempre que la revisión sea mayor que `0`) y el byte `has_custom_serialization` (que se escribe a partir de la revisión `54454`), exactamente igual que en la ruta TCP. Por lo tanto, los Clients no deben dar por hecho que toda carga útil HTTP de `FORMAT Native` corresponde a la revisión `0`.

Dicho de otro modo, los ejemplos de bytes de esta sección que comienzan con un prefijo `BlockInfo` describen la carga útil de un paquete Data de TCP. La misma consulta ejecutada mediante `FORMAT Native` produce la forma abreviada que se muestra junto a ellos.

### BlockInfo

BlockInfo es una secuencia de campos, cada uno precedido por un ID de campo VarUInt, que termina con un ID de campo `0`. El formato wire **no** es autodescriptivo: un ID de campo no codifica la longitud ni el tipo de su valor, por lo que un lector debe conocer de antemano el tipo de cada ID de campo que pueda encontrar. El propio lector de ClickHouse trata un ID de campo no reconocido como un dato corrupto y lanza una excepción (`UNKNOWN_BLOCK_INFO_FIELD`). La compatibilidad hacia adelante se gestiona, en cambio, mediante la revisión del protocolo: el emisor solo escribe un campo si la revisión negociada es igual o superior a la revisión mínima de ese campo, de modo que un receptor más antiguo nunca recibe un campo que no conoce.

| ID de campo | Campo | Tipo | Revisión mínima | Descripción |
| - | - | - | - | - |
| 1 | is\_overflows | UInt8 | 0 | Bloque de desbordamiento de GROUP BY. `0` para los bloques que no son de desbordamiento. |
| 2 | bucket\_number | Int32 | 0 | Bucket de agregación. `-1` para los bloques sin bucket. |
| 3 | out\_of\_order\_buckets | Lista de Int32 | 54480 | Buckets retrasados durante la agregación distribuida. Se codifica como un recuento VarUInt seguido de ese mismo número de valores `Int32`. |
| 0 | (terminador) | — | — | Fin de BlockInfo. Siempre obligatorio. |

Los campos `1` y `2` tienen revisión mínima `0`, por lo que están presentes siempre que se escribe un `BlockInfo`. El campo `3` solo se escribe a partir de la revisión `54480`. Disposición en formato wire para el caso habitual (revisión inferior a `54480`):

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

### Disposición en formato wire de las columnas

Una Column aparece `num_columns` veces dentro de un Bloque.

| # | Campo | Tipo | Condición | Descripción |
| - | - | - | - | - |
| 1 | name | String | siempre | Nombre de la columna |
| 2 | type | String *o* codificación binaria de tipo | siempre | Cadena de tipo de ClickHouse (p. ej., `"UInt64"`, `"Array(String)"`) de forma predeterminada; una codificación binaria de tipo cuando `output_format_native_encode_types_in_binary_format = 1`, excepto en una consulta secundaria (véase la nota más abajo) |
| 3 | has\_custom\_serialization | UInt8 | Funcionalidad `CUSTOM_SERIALIZATION` (v54454) | `0` = predeterminada, `1` = personalizada (le sigue kind\_stack) |
| 4 | kind\_stack | bytes | cuando el campo 3 = `1` | Un byte de enum UInt8 (véase más abajo) que describe la serialización no predeterminada (dispersa, etc.). Para el valor `COMBINATION`, le siguen un recuento VarUInt y esa misma cantidad de bytes de tipo adicionales. Para un `Tuple` (y otros tipos compuestos con información de serialización a nivel de elemento), la carga útil es recursiva; véase más abajo. |
| 5 | data | bytes | siempre | Valores de la columna para las `num_rows` filas. La disposición depende del tipo; consulte [tipos de datos](#data-types). Para columnas dispersas, véase más abajo. |

Un decodificador decide cómo procesar cada columna en función de la cadena `type`. Las cadenas de tipo suelen incluir parámetros entre paréntesis; el decodificador elimina el sufijo `(...)` para obtener el tipo base y, a continuación, analiza los parámetros para determinar el tamaño, la escala o el tipo interno. Para analizar una lista de parámetros con tipos anidados (por ejemplo, un `Tuple` dentro de un `Array`), se necesita un divisor por comas que tenga en cuenta la profundidad y controle el anidamiento de paréntesis, en lugar de una simple división por `,`.

> [!NOTE]
>
> **Codificación binaria de tipos**
>
> El campo `type` es un `String` textual solo en el modo predeterminado. Cuando se establece el ajuste de consulta `output_format_native_encode_types_in_binary_format = 1`, este campo pasa a ser una **codificación binaria de tipo** (la misma codificación basada en etiquetas que se documenta en [codificación binaria de tipos de datos](https://clickhouse.com/docs/es/reference/data-types/data-types-binary-encoding.md)), y las listas aplanadas del tipo `Dynamic` usan esa misma codificación binaria para los nombres de cada tipo. Un decodificador que siempre lea el campo 2 como una cadena con prefijo de longitud interpretaría la primera etiqueta de tipo binaria como la longitud de una cadena y perdería la sincronización, por lo que debe saber qué modo utiliza el stream.
>
> En una consulta con `query_kind = SECONDARY_QUERY` (enviada por otro servidor para `remote`, `Distributed` o réplicas paralelas, o por `clickhouse-client --query_kind secondary_query`), ambos extremos ignoran `output_format_native_encode_types_in_binary_format` e `input_format_native_decode_types_in_binary_format` en los bloques de esa consulta: dichos bloques transportan nombres de tipos en formato textual en ambas direcciones. Los servidores anteriores a esta regla aplican estos ajustes a las consultas secundarias igual que a cualquier otra consulta.

![Enrutamiento del decodificador desde una descripción de tipo hasta su familia de codificación](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 y codificación dispersa

El byte `kind_stack` enumera una serialización por columna no predeterminada:

| Byte | Nombre | Significado | Impacto wire en `data` |
| - | - | - | - |
| `0x00` | DEFAULT | Serialización predeterminada | Idéntico a `has_custom = 0` |
| `0x01` | SPARSE | Serialización dispersa (v54465+) | Stream de desplazamientos + valores no predeterminados; véase más abajo |
| `0x02` | DETACHED | Columna envuelta en un `ColumnBLOB` mediante la serialización paralela de bloques (v54478+) | Blob preserializado: `VarUInt size` + esa cantidad de bytes; véase más abajo |
| `0x03` | DETACHED\_OVER\_SPARSE | Una columna dispersa envuelta en un `ColumnBLOB` | Misma carga útil de blob que `DETACHED`; véase más abajo |
| `0x04` | REPLICATED | Forma de diccionario para valores repetidos (v54482+) | Stream de índices + valores densos de los elementos; véase más abajo |
| `0x05` | COMBINATION | Pila de varios tipos | Seguido de un VarUInt `count` y `count` bytes de tipo adicionales; véase la nota más abajo |

**La carga útil de `COMBINATION` usa un enum diferente.** Las cinco filas anteriores son códigos *compactos* de un byte. `COMBINATION` (`0x05`) es el mecanismo de escape general para cualquier pila que no cubran: va seguido de un `VarUInt` `count` y, a continuación, de `count` entradas de un byte. Esas entradas **no** son los códigos compactos de la tabla, sino los valores sin procesar de `ISerialization::Kind`:

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

Los valores de byte no coinciden con los códigos compactos: `REPLICATED` es `0x03` en este enum anidado, pero `0x04` como código compacto, y no existe ninguna entrada `DETACHED_OVER_SPARSE`; esa combinación aparece como dos entradas consecutivas, `SPARSE` y `DETACHED`. Un decodificador que siga usando la tabla compacta para los bytes anidados asignará mal `0x03`/`0x04` y perderá la sincronización.

`count` es la longitud total de la pila, **incluida la entrada `DEFAULT` inicial** con la que comienza toda pila. Los códigos compactos ya cubren todas las pilas de una y dos entradas, por lo que un `COMBINATION` siempre tiene un `count` de al menos tres.

**Validez de la pila de tipos.** Un decodificador rechaza una pila mal formada con `INCORRECT_DATA` en lugar de construir la serialización que describe:

* la pila comienza con `DEFAULT`;
* la pila es una subsecuencia de `DEFAULT`, `SPARSE`, `REPLICATED`, `DETACHED`, que es el orden en que los tipos se envuelven entre sí. `SPARSE` puede estar dentro de `REPLICATED`, pero no al revés, y `DETACHED` siempre va al final porque un `ColumnBLOB` contiene la forma serializada de todo lo que hay por debajo. Esto también implica que ningún tipo aparece dos veces, por lo que `count` no puede superar el número de tipos.

Tenga en cuenta que este orden no coincide con el de los valores de byte de tipo indicados arriba. Cualquier otra pila describe una disposición de columna que ningún escritor genera y que ningún paso de materialización vuelve a desenvolver en una columna completa del tipo declarado; por ejemplo, un `ColumnSparse` cuya columna de valores sea a su vez dispersa o replicada permanece envuelto.

**`kind_stack` recursivo para columnas `Tuple`.** La carga útil `kind_stack` anterior es el byte (o la secuencia `COMBINATION`) correspondiente a la información de serialización propia de una columna. Un `Tuple` incluye un `SerializationInfoTuple`, que primero escribe la carga útil de pila de tipos *propia* de la tupla y después una carga útil completa de pila de tipos para *cada* elemento, en orden; un decodificador lee esa misma estructura recursiva. Así, para `Tuple(A, B, C)` los bytes del campo 4 son `[tuple_kind][A_kind][B_kind][C_kind]`, y la carga útil de cada elemento es a su vez recursiva si dicho elemento también es un tipo compuesto. El byte `has_custom_serialization` (campo 3) se activa siempre que la información propia de la tupla *o la de cualquiera de sus elementos* no sea la predeterminada, de modo que un `Tuple` cuyo único elemento especial sea disperso o replicado también activa la carga útil de pila de tipos. Un decodificador que lea únicamente el byte de enum inicial de un `Tuple` se detendrá antes de tiempo e interpretará erróneamente los bytes de tipo de los elementos restantes como datos de columna.

**Formato wire disperso.** Cuando `kind_stack = 0x01`, el `data` de la columna consiste en dos streams escritos uno tras otro dentro del único stream TCP compartido:

1. **Stream de desplazamientos** — una secuencia de `VarUInt`s. Cada valor `v` es uno de los siguientes:
   * `v` con el bit alto en la posición 62 sin activar: `(v & 0x3FFFFFFFFFFFFFFF)` = el número de posiciones con valor predeterminado antes del siguiente valor explícito no predeterminado. Esa posición no predeterminada es `cursor + group_size`, donde `cursor` es la posición actual; a continuación, `cursor` avanza `group_size + 1` posiciones.
   * `v` con el bit 62 activado (`END_OF_GRANULE_FLAG`): el valor con el indicador desactivado = el número de posiciones finales con valor predeterminado tras el último valor no predeterminado. Esto marca el final del stream de desplazamientos del bloque.
2. **Stream de valores** — `count` valores no predeterminados codificados de forma densa en el tipo interno, donde `count` es el número de VarUInts no EOG leídos anteriormente.

Un decodificador reconstruye una columna densa de `num_rows` entradas rellenando cada posición no explícita con el valor predeterminado del tipo interno (`0` para enteros y números de coma flotante, `""` para `String`, `0` días para `Date`, etc.).

Una columna `Nullable(T)` dispersa es un caso especial, ya que el valor predeterminado de `Nullable(T)` es **NULL**. La codificación dispersa prescinde por completo del stream habitual del mapa de nulos de `Nullable`: el stream de desplazamientos identifica las posiciones no predeterminadas —es decir, no NULL—, el stream de valores contiene únicamente esos valores no NULL codificados de forma densa en `T`, y cada posición no explícita se reconstruye como NULL. Por lo tanto, un decodificador *no* debe buscar un mapa de nulos en el stream de valores ni rellenar los huecos con un `0` presente, sino con NULL.

**Formato wire Replicated.** Cuando `kind_stack = 0x04`, el `data` de la columna es un diccionario: una lista de valores de elementos distintos junto con un índice por fila que apunta a esa lista (la misma estructura de búsqueda que `LowCardinality`). Cuando el tipo interno está versionado a su vez —por ejemplo, `LowCardinality(T)`—, su prefijo de estado se escribe **primero**, antes del stream de índices: la serialización replicada delega la fase de prefijo en el tipo interno antes de escribir `num_rows`. Los tipos internos con un prefijo vacío (los tipos hoja y los tipos compuestos simples) no aportan ningún byte en este punto.

```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]
```

Un decodificador reconstruye una columna densa seleccionando `elements[indexes[i]]` para cada fila de salida `i`. Los tipos internos compuestos se procesan de forma recursiva: la lista de elementos se materializa en el tipo interno y después se indexa. Entre los tipos internos compatibles se incluyen los tipos hoja, `Nullable(T)`, `Array(T)`, `Tuple(...)`, `Map(K, V)`, `Nested(...)` (cada campo se expande como un `Array`) y `LowCardinality(T)` (se conserva el diccionario compartido; solo se indexan las claves de cada elemento).

**Formato wire separado.** `DETACHED` (`0x02`) y `DETACHED_OVER_SPARSE` (`0x03`) *sí* aparecen en la transmisión por red: no son puramente internos. En la ruta TCP, cuando la compresión está habilitada y la revisión negociada es igual o superior a `DBMS_MIN_REVISON_WITH_PARALLEL_BLOCK_MARSHALLING` (v54478), la columna pasa por tres pasos:

1. Cada columna apta (no `const`, no `Tuple`, en un bloque con más de una fila) se envuelve en un `ColumnBLOB` que contiene la columna ya serializada y comprimida fuera del thread principal.
2. Se añade `DETACHED` al final de la pila de tipos de la columna envuelta.
3. Los `data` de la columna se escriben como un tamaño de blob `VarUInt`, seguido exactamente de esa cantidad de bytes del blob.

Si la columna envuelta era dispersa, su pila es `{DEFAULT, SPARSE, DETACHED}`, que se serializa como `DETACHED_OVER_SPARSE`. Un Client que decodifica una columna de este tipo lee la longitud y los bytes del blob y, a continuación, descomprime el blob para recuperar la carga útil de la columna interna (consulte la [nota sobre `ColumnBLOB`](#compression-negotiation) en la sección de compresión).

`DETACHED` es direccional: solo puede aparecer en los bloques de resultados que envía un servidor, y únicamente para una columna de nivel superior completa, nunca como uno de los tipos de elemento de un `Tuple`. A diferencia de los demás tipos, hace que la columna en tiempo de ejecución sea un `ColumnBLOB` en lugar de una columna del tipo declarado, por lo que un receptor solo lo acepta si su propia canalización vuelve a convertir el blob. Por eso, un servidor rechaza con `INCORRECT_DATA` cualquier bloque enviado por un Client que contenga `DETACHED`, ya sean datos de tablas externas, escalares o datos de inserción.

### Variantes de bloque

Todos los paquetes de la familia Data comparten el mismo formato wire de Block. Las variantes solo se diferencian en el recuento de columnas y de filas:

| Variante | num\_columns | num\_rows | Propósito |
| - | - | - | - |
| Bloque de encabezado | N > 0 | 0 | Anuncia el esquema del resultado (nombres y tipos de las columnas). |
| Bloque de resultado | N > 0 | M > 0 | Filas de resultado propiamente dichas. |
| Bloque vacío | 0 | 0 | Centinela: marca el fin de la entrada en el lado del Client y actúa como marcador de límite en el lado del servidor. |

### Ejemplos a nivel de bytes

Todos los ejemplos de esta sección se han extraído de la **ruta del paquete Data de TCP**, por lo que incluyen el prefijo `BlockInfo` y el byte `has_custom_serialization`. Con `FORMAT Native`, los mismos bloques son más cortos; cuando resulta útil, se indica la forma abreviada equivalente.

Un bloque vacío (con BlockInfo), 8 bytes en 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
```

Un bloque de encabezado para `SELECT 1` anuncia una columna llamada `"1"` de tipo `UInt8`, sin filas. Con versiones de protocolo ≥ 54454, se incluye el byte `has_custom_serialization`:

```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)
```

El bloque de resultados de la misma consulta, con una fila:

```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
```

Con `FORMAT Native` (revisión `0`), el mismo bloque de resultados no incluye `BlockInfo` ni el 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
```

(Un resultado sin filas, como un bloque que solo contiene el encabezado, no produce ningún byte con `FORMAT Native`, ya que el formato de salida no emite bloques vacíos).

## Revisión del protocolo y el formato Native

Lo que más determina la estructura de un stream de bytes Native es la **revisión del protocolo** con la que operan su escritor y su lector. La revisión no figura en ninguna parte de los propios bytes (no existe ningún campo de revisión en el wire), pero aun así decide si varias funcionalidades aparecen o no. Por ello, un decodificador necesita saber con qué revisión se escribió una carga útil antes de poder analizarla. Como la revisión no está en el stream, el lector y el escritor deben acordarla por otra vía.

Se trata de un único `UInt64`, y tanto `NativeWriter` como `NativeReader` lo reciben como argumento del constructor. El escritor lo llama `client_revision` y el lector, `server_revision`, pero es el mismo número. La revisión más reciente que conoce esta versión es `DBMS_TCP_PROTOCOL_VERSION` (consulte `src/Core/ProtocolDefines.h`).

### Qué controla la revisión

Cada funcionalidad depende de un umbral `DBMS_MIN_REVISION_WITH_*`. El escritor emite la funcionalidad solo cuando su revisión alcanza el umbral, y el lector la busca siguiendo exactamente la misma regla, de modo que ambos se mantienen sincronizados; si la revisión es incorrecta en cualquiera de los dos lados, se desincronizan. Los controles relevantes para el formato Native son:

| Funcionalidad | Constante de umbral | Revisión | Efecto por debajo del umbral |
| - | - | - | - |
| Prefijo `BlockInfo` | (cualquier valor `> 0`) | `1` | El prefijo [`BlockInfo`](#blockinfo) se omite por completo; un bloque consta únicamente de `num_columns`, `num_rows` y las columnas. |
| Byte `has_custom_serialization` | `DBMS_MIN_REVISION_WITH_CUSTOM_SERIALIZATION` | `54454` | Se omite el byte [`has_custom_serialization`](#column-wire-layout) de cada columna; todas las columnas usan la serialización predeterminada (sin formas dispersas, replicadas ni separadas). |
| `LowCardinality` en el wire | `DBMS_MIN_REVISION_WITH_LOW_CARDINALITY_TYPE` | `54405` | Caso especial: **no** sigue la regla simple del umbral. `LowCardinality(T)` se reduce al tipo base `T` solo cuando la revisión es *distinta de cero* e inferior a `54405`, o cuando la reducción se fuerza por separado. Con la revisión `0` se conserva. Consulte la nota a continuación. |
| Serialización V2 de `Dynamic` / `JSON` | `DBMS_MIN_REVISION_WITH_V2_DYNAMIC_AND_JSON_SERIALIZATION` | `54473` | `Dynamic` y `JSON`/`Object` usan la serialización V1 (con el parámetro `max_dynamic_*`) en lugar de la V2. |
| Serialización de `String` con desplazamientos | `DBMS_MIN_REVISION_WITH_STRING_WITH_SIZE_STREAM_SERIALIZATION` | `54492` | Por debajo del umbral, los datos de columnas `String` usan la disposición por valor (prefijo de longitud `VarUInt` + bytes sin procesar por fila) en lugar de la [disposición de desplazamientos](#string-type) (desplazamientos de bytes acumulados como `UInt64`, seguidos de todos los datos concatenados). Depende únicamente de la revisión, sin ningún marcador por columna en el wire. A partir del umbral, la disposición de desplazamientos también se aplica a los `String` anidados dentro de tipos compuestos (`Array`, `Nullable`, `Map`, `Tuple`, `Variant`, `Dynamic`, `JSON`), pero **no** al diccionario de una columna `LowCardinality`: un diccionario `LowCardinality` siempre se escribe con la serialización anidada predeterminada, por lo que sus valores `String` conservan la disposición por valor (esto incluye `LowCardinality(String)` y `LowCardinality(Nullable(String))`). El formato `Buffers` no forma parte del protocolo y siempre usa la disposición por valor. |
| Control de versiones de funciones de agregación | `DBMS_MIN_REVISION_WITH_AGGREGATE_FUNCTIONS_VERSIONING` | `54452` | El estado de `AggregateFunction` se escribe sin una versión incrustada. |
| `skip_degree` en el estado de `quantileDeterministic` | `DBMS_MIN_REVISION_WITH_QUANTILE_DETERMINISTIC_SKIP_DEGREE` | `54491` | Los estados de `AggregateFunction(quantileDeterministic, ...)` (y las variantes `quantiles`/`median` de la misma función) se escriben con la versión de estado `0`, sin el `UInt8 skip_degree` final. Consulte la nota en [alias de tipos](#type-aliases). |
| Muestra de hashes de 64 bits en el estado de `uniq` | `DBMS_MIN_REVISION_WITH_UNIQ_STATE_VERSION_1` | `54493` | Los estados de `AggregateFunction(uniq, ...)` se escriben con la versión de estado `0`: sin el byte de indicadores inicial ni la muestra final de hashes de 64 bits. Consulte la nota en [alias de tipos](#type-aliases). |
| `out_of_order_buckets` en `BlockInfo` | `DBMS_MIN_REVISION_WITH_OUT_OF_ORDER_BUCKETS_IN_AGGREGATION` | `54480` | No se escribe el campo de `BlockInfo` con ID `3` (consulte [BlockInfo](#blockinfo)). |
| Serialización paralela de bloques (`DETACHED`) | `DBMS_MIN_REVISON_WITH_PARALLEL_BLOCK_MARSHALLING` | `54478` | Las columnas nunca se encapsulan en un `ColumnBLOB`; no aparecen los tipos `DETACHED` / `DETACHED_OVER_SPARSE` (consulte [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` | El parámetro de zona horaria se elimina de la cadena `type`: `DateTime('UTC')` se anuncia simplemente como `DateTime`. |

Esto convierte a la revisión `0` en la codificación más conservadora para casi todo: el stream no incluye `BlockInfo` ni el byte `has_custom_serialization`, usa `Dynamic`/`JSON` V1, no incluye versión de función de agregación y emplea un `DateTime` simple, sin el parámetro de zona horaria. Las versiones del *estado* de las funciones de agregación son la excepción en la revisión `0`: no hay ningún interlocutor del que derivar una versión —el stream lo escribe y lo vuelve a leer quien lo produjo (un archivo de datos `StripeLog`, una copia de seguridad `Set`/`Join`, un archivo en formato `Native`)—, por lo que el escritor conserva la versión fijada en el tipo (indicándola explícitamente en la cadena de tipo, como de costumbre) en lugar de degradar el estado a la versión `0`. Solo los tipos que no fijan ninguna versión se escriben con la versión `0`.

`LowCardinality` es la única excepción, y una importante. La comprobación del escritor es `remove_low_cardinality || (client_revision && client_revision < DBMS_MIN_REVISION_WITH_LOW_CARDINALITY_TYPE)`. La clave está en el `client_revision &&` inicial: cuando la revisión es exactamente `0`, la evaluación en cortocircuito hace que toda la condición resulte falsa.

Por tanto, en la revisión `0` —el valor predeterminado para `FORMAT Native`— `LowCardinality(T)` **no** se elimina. Su cadena de tipo y el prefijo de estado por bloque se mantienen en el stream, y el lector de la revisión `0` los vuelve a leer tal cual. La eliminación solo se produce con una revisión distinta de cero inferior a `54405`, o cuando se fuerza con independencia de la revisión.

Ese forzado se controla mediante el indicador `remove_low_cardinality`. La salida de `FORMAT Native` nunca lo activa, pero la ruta del protocolo TCP nativo sí lo hace cuando `low_cardinality_allow_in_native_format = 0` (valor predeterminado: `1`). Es decir, esa configuración modifica la salida por TCP nativo, pero no afecta en absoluto a `FORMAT Native`.

En la práctica: un stream `FORMAT Native` predeterminado puede contener `LowCardinality` de forma legítima, así que no dé por hecho que se trata de una funcionalidad ausente en la revisión `0`.

### De dónde procede la revisión según cómo viajen los datos

Los mismos bytes en formato Native pueden transmitirse por distintas vías: el protocolo TCP nativo, una solicitud HTTP o un archivo en disco. Cada vía establece la revisión a su manera. Conviene tener en cuenta algo: el lado de lectura y el de escritura se configuran por separado, por lo que pueden terminar con revisiones distintas.

#### Protocolo TCP nativo — negociado, en ambas direcciones

En el [protocolo TCP nativo](https://clickhouse.com/docs/es/resources/develop-contribute/native-protocol/protocol.md), la revisión se obtiene del handshake Hello. El Client envía `DBMS_TCP_PROTOCOL_VERSION`, el servidor responde con la suya y, a partir de ese momento, cada lado serializa según la **revisión anunciada por el otro lado**: el servidor construye su `NativeReader`/`NativeWriter` a partir de `client_tcp_protocol_version`, mientras que el Client usa la `server_revision` que recibió. No existe un `min` explícito, pero ningún lado puede emitir una funcionalidad que no haya implementado, por lo que, en la práctica, cada dirección queda limitada por el más antiguo de los dos extremos.

Cuando ambos extremos ejecutan el mismo build moderno, las dos direcciones coinciden en la misma revisión (`DBMS_TCP_PROTOCOL_VERSION`, consulte `src/Core/ProtocolDefines.h`) y todas las condiciones están activas. Es el caso habitual, pero no está garantizado. Si los extremos tienen versiones distintas o son de terceros, cada dirección puede estar en una revisión diferente, por lo que las condiciones deben evaluarse por separado en cada dirección: `BlockInfo` está presente en cualquier revisión distinta de cero, pero el resto —incluido `has_custom_serialization`— solo aparece cuando la revisión efectiva de esa dirección alcanza los umbrales correspondientes. Por ejemplo, un extremo que anuncia una revisión inferior a `54454` no envía ni recibe el byte `has_custom_serialization`.

#### Salida `FORMAT Native`: revisión 0 de forma predeterminada, ampliable a través de HTTP

El formato de *salida* `Native` usa de forma predeterminada la revisión **`0`**. Esto abarca `SELECT ... FORMAT Native` a través de HTTP, `INTO OUTFILE ... FORMAT Native` y la salida `Native` que escribe `clickhouse-client`; en todos los casos, la fábrica de salida pasa `FormatSettings::client_protocol_version` directamente al `NativeWriter`, que determina el entramado de `BlockInfo` / `has_custom_serialization` *y* la disposición de `String` con desplazamientos. Así, una consulta HTTP `SELECT ... FORMAT Native` con un `client_protocol_version` elevado (≥ `54492`) escribe la disposición con desplazamientos, mientras que la revisión predeterminada `0` conserva la disposición portable por valor.

A través de HTTP, ese valor predeterminado no es la última palabra. Un Client puede elevarlo con el parámetro de consulta `?client_protocol_version=<n>`, que el handler HTTP trata como un parámetro reservado y no como un ajuste SQL: se asigna al contexto de la consulta y la capa de formato lo copia en `FormatSettings`. Con un valor lo bastante alto, la salida HTTP `FORMAT Native` empieza a incluir el prefijo `BlockInfo` y el byte `has_custom_serialization`, igual que la ruta TCP; por tanto, no dé por hecho que una carga útil HTTP `FORMAT Native` es siempre de revisión `0`.

Como el parámetro se asigna a todo el contexto de la consulta, llega a cada `NativeWriter` creado para esa solicitud, no solo al stream de resultados. En concreto, una escritura del lado del servidor como `INSERT INTO FUNCTION file('x.native', 'Native', ...) SELECT ...` a través de HTTP con un `?client_protocol_version=` elevado escribe el archivo con esa revisión. Se trata de un comportamiento antiguo (anterior a la serialización de String con desplazamientos y que también afecta a `BlockInfo` y `has_custom_serialization`), y además asimétrico: el lado de lectura (véase más abajo) no se eleva de la misma manera, por lo que ese archivo no puede volver a leerse mediante `file(...)`. No incluya `client_protocol_version` en solicitudes que escriban datos que vayan a leerse de nuevo. `INTO OUTFILE` y la salida local de `clickhouse-client` no ofrecen ese ajuste y se mantienen en `0`.

#### Entrada `FORMAT Native`: siempre revisión 0

El formato de *entrada* `Native` siempre construye su `NativeReader` con la revisión **`0`**. Nunca espera un prefijo `BlockInfo`, nunca lee el byte `has_custom_serialization` y siempre asume la serialización predeterminada, tanto si analiza el cuerpo de un `INSERT ... FORMAT Native` como si lee un archivo `Native` o infiere un esquema. `?client_protocol_version=<n>` no eleva la revisión de entrada (solo el lado de salida lee ese parámetro).

Como la revisión de entrada es siempre `0`, el formato de entrada siempre lee la disposición `String` por valor. Un stream escrito con la disposición de desplazamientos (a través del protocolo TCP nativo o mediante un `SELECT ... FORMAT Native` con un `client_protocol_version` elevado) no puede volver a leerse mediante la entrada `FORMAT Native`: se trata de la misma asimetría que ocurre con el entramado de `BlockInfo`.

### Implicaciones para el ida y vuelta

Para `FORMAT Native`, el valor predeterminado es la revisión `0` en ambos extremos. Los datos escritos por `SELECT ... FORMAT Native` con la revisión `0` se pueden volver a leer directamente con `INSERT ... FORMAT Native` sin sorpresas.

Un stream generado con una revisión de salida elevada (`?client_protocol_version=<n>` en el `SELECT`) incluye el entramado de `BlockInfo` y `has_custom_serialization`, así como la disposición de `String` con desplazamientos, y el formato de entrada (siempre en la revisión `0`) no puede leer ninguno de ellos. Por lo tanto, elevar `client_protocol_version` solo tiene sentido para un consumidor que utilice el protocolo TCP nativo, no para un ida y vuelta de `FORMAT Native` sobre HTTP.

Los archivos son el caso asimétrico. Un `?client_protocol_version=` elevado en un `INSERT INTO FUNCTION file(...)` del lado del servidor (o `s3`, `url`) *escribe* el archivo con esa revisión, pero una *lectura* de `file(...)` siempre analiza con la revisión `0`, por lo que ese archivo no admite un ida y vuelta. O bien omita `client_protocol_version` en la solicitud de escritura para que el archivo quede en la revisión `0` estándar, o bien transfiera los datos mediante el protocolo TCP nativo, donde cada dirección utiliza la revisión negociada en el handshake.

| Canal | Revisión de escritura | Revisión de lectura | `BlockInfo` / serialización personalizada |
| - | - | - | - |
| Paquete Data de TCP nativo | Revisión anunciada por el par (por dirección) | Revisión anunciada por el par (por dirección) | `BlockInfo` siempre que la revisión sea `> 0`; `has_custom_serialization` a partir de `≥ 54454` |
| `SELECT ... FORMAT Native` sobre HTTP | `client_protocol_version` (predeterminado `0`) | n/a | Solo si se eleva `client_protocol_version` |
| `INSERT ... FORMAT Native` sobre HTTP (cuerpo) | n/a | `0` (siempre) | Sin entramado; `String` siempre por valor |
| `INSERT INTO FUNCTION file`/`s3`/`url(..., 'Native')` sobre HTTP | `client_protocol_version` (predeterminado `0`) | `0` (las lecturas, siempre) | En escritura, solo si se eleva `client_protocol_version`; ese archivo no se puede volver a leer |
| `INTO OUTFILE` / `clickhouse-client` local `FORMAT Native` | `0` | `0` | Ausente (aunque `LowCardinality` se conserva; consulte la nota anterior) |

> [!NOTE]
>
> **Revisión del protocolo frente a versión de serialización**
>
> No confunda la revisión del protocolo con la [versión de serialización](#serialization-version-concept). La revisión de la que hablamos aquí se aplica a toda la conexión o solicitud y nunca aparece en los bytes. La versión de serialización es por columna, la transportan los [tipos versionados](#versioned-types) y se escribe en cada bloque no vacío. La revisión determina si una funcionalidad está presente o no; la versión de serialización, una vez dentro de una columna versionada, determina qué variante de la codificación de ese tipo concreto viene a continuación.

## Tipos de datos

Esta sección documenta la codificación wire de los tipos que el formato Native puede transportar dentro del `data` de una columna, agrupados en cuatro familias según la complejidad creciente que suponen para el decodificador. Dos tipos, `AggregateFunction(func, ...)` y `QBit(T, N[, stride])`, son tipos de columna `Native` válidos, pero tienen cargas útiles específicas de cada función o tipo que quedan fuera del alcance de este documento; se indican más abajo en los puntos donde podrían confundirse con alias.

| Familia | Sección | Flujos por columna | Estado entre bloques |
| - | - | - | - |
| Ancho fijo | [Tipos de ancho fijo](#fixed-width-types) | Uno | Ninguno |
| Longitud variable | [Tipos de longitud variable](#variable-length-types) | Uno | Ninguno |
| Compuesto (forma fija) | [Tipos compuestos](#composite-types) | Varios | Ninguno |
| Versionado / con estado | [Tipos versionados](#versioned-types) | Varios | Ninguno en el wire Native: prefijo de estado por bloque, reiniciado en cada bloque |

### Tipos de ancho fijo

Cada valor ocupa un número constante de bytes. Una columna de `M` filas ocupa exactamente `bytes_per_row × M` bytes en el wire, concatenados sin separadores ni relleno.

| Cadena de tipo | Bytes por valor | Valor lógico | Codificación wire |
| - | - | - | - |
| `UInt8` | 1 | Integer sin signo de 8 bits | Byte sin procesar |
| `UInt16` | 2 | Integer sin signo de 16 bits | Little-endian |
| `UInt32` | 4 | Integer sin signo de 32 bits | Little-endian |
| `UInt64` | 8 | Integer sin signo de 64 bits | Little-endian |
| `UInt128` | 16 | Integer sin signo de 128 bits | Little-endian |
| `UInt256` | 32 | Integer sin signo de 256 bits | Little-endian |
| `Int8` | 1 | Con signo de 8 bits, en complemento a dos | Byte sin procesar |
| `Int16` | 2 | Con signo de 16 bits, en complemento a dos | Little-endian |
| `Int32` | 4 | Con signo de 32 bits, en complemento a dos | Little-endian |
| `Int64` | 8 | Con signo de 64 bits, en complemento a dos | Little-endian |
| `Int128` | 16 | Con signo de 128 bits, en complemento a dos | Little-endian |
| `Int256` | 32 | Con signo de 256 bits, en complemento a dos | Little-endian |
| `Float32` | 4 | IEEE 754 de precisión simple | Little-endian |
| `Float64` | 8 | IEEE 754 de precisión doble | Little-endian |
| `BFloat16` | 2 | 16 bits más significativos de un `Float32` IEEE 754 | Little-endian |
| `Bool` | 1 | `0x00` = false, `0x01` = true | Byte sin procesar |
| `Date` | 2 | Días desde `1970-01-01` | UInt16 little-endian |
| `Date32` | 4 | Días desde `1970-01-01` (con signo; admite fechas anteriores a 1970) | Int32 little-endian |
| `DateTime` | 4 | Marca de tiempo Unix en segundos | UInt32 little-endian |
| `DateTime(tz)` | 4 | Igual que `DateTime`; la zona horaria es metadata | UInt32 little-endian |
| `DateTime64(s)` | 8 | Ticks en la escala `s` (10^-s segundos desde la época Unix) | Int64 little-endian |
| `DateTime64(s, tz)` | 8 | Igual que `DateTime64(s)`; la zona horaria es metadata | Int64 little-endian |
| `Time` | 4 | Duración de reloj con signo, en segundos | Int32 little-endian |
| `Time64(s)` | 8 | Duración de reloj con signo, en ticks en la escala `s` | Int64 little-endian |
| `Interval<Unit>` | 8 | Recuento con signo; la unidad se indica en la cadena de tipo | Int64 little-endian |
| `UUID` | 16 | Identificador de 128 bits | Dos mitades UInt64 LE con los bytes intercambiados (consulte [UUID](#uuid)) |
| `IPv4` | 4 | Dirección IPv4 | UInt32 little-endian |
| `IPv6` | 16 | Dirección IPv6 | Orden de bytes de red, sin intercambio |
| `Enum8` | 1 | Con signo de 8 bits (índice de variante) | Byte sin procesar |
| `Enum16` | 2 | Con signo de 16 bits (índice de variante) | Little-endian |
| `Decimal(P, S)` | 4 / 8 / 16 / 32 | `value × 10^S` como integer con signo; el ancho depende de P (≤9 → 4 B, ≤18 → 8 B, ≤38 → 16 B, ≤76 → 32 B) | Integer con signo little-endian |

#### Tipos integer

`UInt8`–`UInt256` e `Int8`–`Int256` son codificaciones binarias directas de valores integer. Un decodificador lee `bytes_per_row × num_rows` bytes y los interpreta según el tipo.

Una columna `UInt32` con los 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
```

Una columna `Int8` que contiene `[-1, 42]`:

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

#### Float32 y Float64

Números de coma flotante binarios estándar IEEE 754: 4 bytes en precisión simple (`binary32`) y 8 bytes en doble precisión (`binary64`), ambos en little-endian. NaN, ±Infinity, ±0.0 y los números subnormales se conservan intactos en el ida y vuelta, sin normalización.

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

El formato brain-floating-point: los 16 bits superiores de un `Float32` IEEE 754 (1 bit de signo, 8 bits de exponente y 7 bits de mantisa). Cada valor ocupa 2 bytes, en little-endian, y contiene el patrón de 16 bits sin procesar. Para recuperar el valor numérico, amplíelo de nuevo a `Float32` colocando el patrón en la mitad superior y poniendo a cero la mitad inferior (`bits << 16` reinterpretado como `Float32`); a partir de ahí, el valor ampliado usa el mismo formato de texto que `Float32`.

Valor `BFloat16` `1.5` (patrón `0x3FC0`, la mitad superior del `Float32` `0x3FC00000`):

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

#### Bool

Compatible a nivel de wire con `UInt8`: 1 byte por fila, `0x00` = false, `0x01` = true. En el wire, la cadena de tipo es literalmente `Bool` (no `UInt8`), por lo que un decodificador que seleccione la ruta de decodificación según la cadena de tipo debe reconocerla por separado.

Una columna `Bool` `[true, false, true]`:

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

#### Date y Date32

Ambos codifican las fechas como un recuento integer de días respecto a la época Unix `1970-01-01`. Ninguno de los dos incluye un componente de hora.

| Tipo | Bytes | Codificación | Rango |
| - | - | - | - |
| `Date` | 2 | UInt16 little-endian | `1970-01-01` a `2149-06-06` |
| `Date32` | 4 | Int32 little-endian | rango amplio con signo, admite fechas anteriores a 1970 |

Valor `Date` `1970-01-02` (1 día):

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

Valor `Date32` `1900-01-01` (-25567 días):

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

#### DateTime

Compatible a nivel de wire con `UInt32`: una marca de tiempo Unix en segundos, de 4 bytes en little-endian. El tipo puede aparecer como `DateTime` o `DateTime('Timezone')`; la zona horaria solo afecta a la visualización y no forma parte del valor en el wire. Dos columnas `DateTime` con distintos parámetros de zona horaria generan bytes idénticos para un mismo instante. Un decodificador elimina el sufijo de parámetro `(...)` y procesa la columna como `UInt32`.

Valor `2024-03-15 14:30:00 UTC` de tipo `DateTime('UTC')` (marca de tiempo `1710513000`):

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

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

8 bytes: un Int64 en formato little-endian que representa ticks a escala de `10^-scale` segundos desde la época Unix. El parámetro `scale` (0–9) forma parte de la cadena de tipo y define la unidad de tiempo:

| Escala | Tamaño del tick | Nombre común |
| - | - | - |
| 0 | 1 segundo | segundos |
| 3 | 1 milisegundo | ms |
| 6 | 1 microsegundo | µs |
| 9 | 1 nanosegundo | ns |

El tipo aparece como `DateTime64(s)` (zona horaria predeterminada del servidor, implícita) o como `DateTime64(s, 'TimezoneName')` (zona horaria explícita, solo a efectos de visualización). Los valores negativos representan ticks anteriores a la época.

Valor `DateTime64(3, 'UTC')` correspondiente a `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 y Time64(scale)

Una duración de reloj, no un momento dado. `Time` es un recuento de segundos con signo, almacenado en 4 bytes como Int32 little-endian; `Time64(scale)` es un recuento de ticks con signo en la escala decimal indicada (0–9), almacenado en 8 bytes como Int64 little-endian, es decir, la misma forma en wire que `DateTime64`.

La forma textual es `[-]HH:MM:SS[.fraction]`, pero, a diferencia de `DateTime`, el campo de la hora **no** se reinicia al completar un día de 24 horas: representa el número total de horas y puede ser mayor que 23. La magnitud mostrada tiene un tope de `999:59:59` (`3599999` segundos); una magnitud mayor se representa con los segundos en el tope y conserva tal cual la fracción almacenada, de modo que un recuento de ticks de `3700000.25` se representa como `999:59:59.250000`. `CAST` satura el valor almacenado al mayor tick representable en la escala de destino (`999:59:59.999999` para `Time64(6)`), y no al segundo entero, mientras que las operaciones aritméticas aún pueden dejar en la columna un recuento de ticks fuera de rango que solo se limita al mostrarse. Nada de esto afecta a los bytes en wire, que son simplemente el integer con signo.

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` y `Time64` son experimentales y requieren `allow_experimental_time_time64_type = 1` en el servidor.

#### Interval

`Interval<Unit>` — `IntervalSecond`, `IntervalMinute`, `IntervalHour`, `IntervalDay`, `IntervalWeek`, `IntervalMonth`, `IntervalQuarter`, `IntervalYear`, `IntervalNanosecond`, etc. Todas las unidades comparten la misma codificación wire: el recuento como un Int64 con signo de 8 bytes en little-endian. La unidad figura **únicamente** en la cadena de tipo: no altera ni los bytes wire ni la forma textual, que es simplemente el integer. Una única ruta de decodificación sirve para todas las unidades.

Valor `5` de `IntervalDay`:

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

#### UUID

16 bytes por valor. La codificación wire **no** corresponde a los 16 bytes canónicos en big-endian: el orden de los bytes de cada mitad de 8 bytes se invierte de forma independiente.

El modelo lógico es un identificador de 128 bits en su forma de texto canónica `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx`, en la que, por convención, los bytes se escriben en big-endian. El modelo wire toma esos 16 bytes canónicos, los divide en dos mitades de 8 bytes y escribe cada mitad en little-endian:

* Bytes wire 0..7 = bytes canónicos 0..7 en orden inverso.
* Bytes wire 8..15 = bytes canónicos 8..15 en orden inverso.

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
```

El UUID nulo (todo ceros) se representa de forma idéntica en ambas representaciones.

#### IPv4 e IPv6

Dos tipos de dirección relacionados, pero con codificaciones distintas.

`IPv4` ocupa 4 bytes y se codifica como un UInt32 little-endian que contiene la dirección canónica de 32 bits (el valor `(a << 24) | (b << 16) | (c << 8) | d` obtenido de `a.b.c.d`). Los bytes en el wire son los bytes en orden de red, pero en orden inverso.

`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 y se escribe **tal cual, en orden de bytes de red**, sin intercambio de bytes: el mismo orden de bytes que `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
```

La asimetría es intencionada: IPv4 se almacena como un `u32` para permitir operaciones aritméticas y consultas de rango compactas, mientras que IPv6 mantiene la disposición en orden de red habitual en la mayoría de las API de red.

#### Enum8 y Enum16

Compatibles a nivel de wire con `Int8` e `Int16`, respectivamente: 1 o 2 bytes por fila, en complemento a dos little-endian para la variante de 16 bits. La correspondencia completa de valores se encuentra en la cadena de tipo:

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

Un decodificador puede eliminar el sufijo de parámetros `(...)` y despacharlo como `Int8` / `Int16`: los wire bytes son simplemente el índice integer. Un client que expone la etiqueta analiza el mapa `'name' = value` a partir de la cadena de tipo y lo conserva junto con la columna, ya que el integer por sí solo no permite recuperar la etiqueta. La salida orientada a texto muestra la etiqueta (`active`) en lugar del índice, entre comillas simples (`'active'`) cuando el enum está anidado dentro de un tipo compuesto. Como el mapa no puede recuperarse a partir de la columna de integers, debe conservarse en el caso de enums anidados como `Array(Enum8(...))` o `Map(Enum16(...), V)`.

Una columna `Enum8('active' = 1, 'inactive' = 2)` con los valores `[active, inactive, active]`:

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

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

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

#### Decimal(P, S)

Un integer con signo escalado por una potencia de 10. El ancho en bytes del integer viene determinado por la **precisión** `P`; la **escala** `S` es el exponente negativo (el número de dígitos después del punto decimal). Ambos parámetros se indican en la cadena de tipo.

| Precisión (P) | Integer subyacente | Bytes |
| - | - | - |
| 1 ≤ P ≤ 9 | Int32 | 4 |
| 10 ≤ P ≤ 18 | Int64 | 8 |
| 19 ≤ P ≤ 38 | Int128 | 16 |
| 39 ≤ P ≤ 76 | Int256 | 32 |

La codificación wire es el integer subyacente en complemento a dos little-endian, y el valor decimal lógico es `wire_integer × 10^(-S)`.

ClickHouse siempre emite `Decimal(P, S)`, independientemente de cómo se haya declarado el tipo. `Decimal32(S)`, `Decimal64(S)`, etc., se normalizan todos a `Decimal(P, S)` en el wire (con `P` fijado en el máximo natural para cada ancho: 9, 18, 38, 76). Un decodificador que solo reconozca `Decimal(P, S)` cubre todas las variantes que emite el servidor.

El valor `123.4567` de tipo `Decimal(9, 4)` → integer subyacente `1234567`:

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

Valor `-1.5` de tipo `Decimal(18, 1)` → integer subyacente `-15`:

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

Valor `Decimal(38, 4)` `123.4567` (16 bytes en total):

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

#### Nothing

El tipo `Nothing` no contiene valores. En la práctica, solo aparece como tipo interno de `Nullable(Nothing)`, que es lo que el servidor devuelve para una expresión como `SELECT NULL`, cuyo único valor válido es precisamente la ausencia de valor. Conceptualmente, se trata de un tipo unitario.

En el wire ocupa exactamente **un byte de relleno por fila**. El servidor emite el carácter ASCII `'0'` (`0x30`), pero el deserializador ignora estos bytes: su contenido no está definido y los decodificadores no deben depender de ningún valor concreto. El número de bytes escritos es `num_rows × 1`, por lo que el `num_rows` del encabezado de la columna determina por completo cuántos bytes hay que consumir.

Este byte por fila preserva el invariante del bloque: cada columna ocupa una longitud que puede derivarse de `num_rows`, de modo que los decodificadores avanzan secuencialmente sin necesidad de prefijos de longitud por celda. El `Nullable` que lo envuelve siempre marca todas las posiciones como NULL, por lo que los bytes de relleno nunca llegan a inspeccionarse.

Una columna `Nullable(Nothing)` con 3 filas (todas NULL):

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

El prefijo del mapa de nulos es la estructura de tramas estándar de `Nullable` (consulte [Nullable](#nullable)); los tres bytes internos corresponden a la carga útil de `Nothing`, que el decodificador omite.

### Tipos de longitud variable

Cada valor incluye su propia longitud en la representación wire.

#### String

Cadena de tipo: `String`. Una columna `String` tiene dos disposiciones en formato wire, que se seleccionan según la revisión del protocolo (consulte [Qué controla la revisión](#what-the-revision-gates)). La revisión es la negociada en el protocolo TCP nativo y, en el caso de la salida `FORMAT Native`, la indicada por `client_protocol_version` (valor predeterminado `0`); la entrada `FORMAT Native` siempre usa la revisión `0`, y el formato `Buffers` siempre utiliza la disposición por valor.

**Disposición por valor** — por debajo de `DBMS_MIN_REVISION_WITH_STRING_WITH_SIZE_STREAM_SERIALIZATION` (`54492`), lo que incluye la revisión `0` predeterminada de `FORMAT Native`: una secuencia de `num_rows` secuencias de bytes, cada una precedida por su longitud:

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

No hay separadores entre filas aparte de los prefijos de longitud, ni ningún estado a nivel de fila. Una cadena vacía se representa con un único byte `0x00`. El número total de bytes que ocupa la columna es `Σ (varuint_size(len_i) + len_i)` para todas las filas.

Una columna de 3 cadenas `["ab", "", "c"]` (6 bytes en 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"
```

**Disposición de desplazamientos**: a partir de la revisión `54492`, dos flujos concatenados; primero, los desplazamientos acumulados en bytes y, a continuación, todos los datos:

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

Los desplazamientos se envían tal cual, de la misma forma en que un `Array` envía sus desplazamientos a través del protocolo nativo. El desplazamiento `i` es la posición final del valor `i` en el blob de datos, por lo que el tamaño del valor `i` es `offset[i] - offset[i-1]` (con `offset[-1] = 0`) y el último desplazamiento es el tamaño total del blob de datos. Un decodificador lee `8 × num_rows` bytes de desplazamientos y, a continuación, lee todo el blob de datos de una sola vez, usando el último desplazamiento como su longitud exacta. Esto permite preasignar el búfer con exactitud y realizar copias masivas en lugar de analizar fila por fila, sin necesidad de convertir tamaños en desplazamientos. Una columna con `num_rows = 0` no aporta ningún byte en ninguno de los dos flujos.

Las mismas 3 cadenas `["ab", "", "c"]` (27 bytes en 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"
```

Cuando se aplica la disposición de desplazamientos, esta afecta a todos los `String` del bloque, incluidos los `String` anidados dentro de tipos compuestos (`Array(String)`, `Nullable(String)`, `Map`, `Tuple`, `Variant`, `Dynamic`, `JSON`), con una excepción: el diccionario de una columna [`LowCardinality`](#lowcardinality) siempre se escribe con la serialización anidada predeterminada, por lo que sus valores `String` mantienen la disposición por valor en todas las revisiones. Esto incluye `LowCardinality(String)`, `LowCardinality(Nullable(String))` y cualquier otro diccionario `LowCardinality` que contenga valores String.

En ambas disposiciones, el tipo `String` de ClickHouse está orientado a bytes, no a texto: no se exige que los valores sean UTF-8 válidos, y un valor puede contener cualquier byte, incluidos caracteres NUL incrustados. Un decodificador que asigne los datos a un tipo de cadena UTF-8 debe validarlos durante la lectura o bien exponer los bytes sin procesar al llamador.

#### FixedString(N)

Cadena de tipo: `FixedString(N)`, donde `N` es un integer positivo (por ejemplo, `FixedString(16)`). La columna ocupa exactamente `N × num_rows` bytes sin procesar, sin prefijos de longitud ni separadores. Un decodificador analiza `N` a partir de la cadena de tipo y consume esa cantidad de bytes por fila.

Cuando la sentencia SQL inserta un valor de menos de `N` bytes (por ejemplo, `CAST('abc' AS FixedString(5))`), el servidor lo rellena por la derecha con bytes NUL (`0x00`) hasta alcanzar la longitud declarada. Estos bytes de relleno forman parte del valor almacenado y se transmiten por el wire tal cual; eliminarlos es responsabilidad del client. Al igual que `String`, `FixedString(N)` se comporta más como un array de bytes que como texto; suele usarse para identificadores de ancho fijo, bytes de direcciones o digests de hash.

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

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

Comparación de los dos tipos de cadena:

| Propiedad | `String` | `FixedString(N)` |
| - | - | - |
| Prefijo de longitud por fila | Sí (VarUInt), o un stream de desplazamientos independiente a partir de la revisión `54492` | No |
| Tamaño de fila | Variable | Exactamente `N` bytes |
| Bytes totales de la columna | Variable | `N × num_rows` |
| Relleno con bytes NUL | n/a | El servidor rellena por la derecha |
| UTF-8 esperado | Normalmente (no se exige) | No (tratar como bytes sin procesar) |
| Parámetro de tipo | Ninguno | Integer `N` obligatorio |

### Tipos compuestos

Los tipos compuestos envuelven uno o más tipos internos y comparten un modelo wire común: **múltiples flujos por columna**. Una única columna lógica se codifica como dos o más secuencias de bytes concatenadas que se leen de forma independiente.

Comparten tres propiedades estructurales:

* **Forma fija según el esquema.** La estructura queda determinada por completo por la cadena de tipo en el momento de la decodificación. `Array(UInt32)` siempre tiene la misma disposición de flujos, de un bloque a otro.
* **Sin prefijo de versión propio.** El envoltorio compuesto en sí no añade ningún byte de versión; su estructura de tramas (desplazamientos, mapa de nulos, flujos de elementos) se mantiene estable entre versiones de ClickHouse. Esto se aplica únicamente al *envoltorio*; para los tipos internos versionados, consulte más abajo la nota sobre la fase de prefijo.
* **Sin estado propio entre bloques.** La estructura de tramas del envoltorio es totalmente autodescriptiva en cada bloque; cualquier cuestión relativa al estado entre bloques proviene de un tipo interno versionado, no del envoltorio.

Los tipos compuestos son recursivos: un tipo interno puede ser, a su vez, un tipo compuesto.

**Fase de prefijo antes de los flujos de datos.** La lectura de una columna consta de dos fases, en este orden: una **fase de prefijo de estado** y, a continuación, la **fase de flujos de datos**. Un envoltorio compuesto no tiene bytes de prefijo propios, pero *delega* la fase de prefijo en su serialización interna antes de escribir cualquiera de sus propios flujos de datos: `SerializationArray` ejecuta la fase de prefijo de su tipo interno antes de escribir los desplazamientos del array, y `Tuple`, `Map`, `Nested` y `Nullable` hacen lo mismo a través de las serializaciones de sus elementos (`Nullable` ejecuta el prefijo interno antes de su mapa de nulos).

Por lo tanto, cuando un tipo compuesto envuelve un [tipo versionado/con estado](#versioned-types) (`LowCardinality`, `Variant`, `Dynamic`, `JSON`), el prefijo de versión/estado de ese tipo interno se emite *primero*, antes de los desplazamientos y de la carga útil de elementos del envoltorio. Por ejemplo, `Array(LowCardinality(String))` se dispone como `[LowCardinality state prefix]` → `[array offsets]` → `[flattened LowCardinality element payload]`, y no con los desplazamientos en primer lugar.

Un decodificador que lea los desplazamientos antes de ejecutar la fase de prefijo interna perderá la sincronización con cualquier tipo compuesto que contenga `LowCardinality`, `Variant`, `Dynamic` o `JSON`. Cuando todos los tipos internos son hojas simples u otros tipos compuestos no versionados, la fase de prefijo no emite ningún byte y la descripción que aparece a continuación, con los desplazamientos en primer lugar, se aplica tal cual.

#### Nullable(T)

Cadena de tipo: `Nullable(InnerType)`. Ejemplos: `Nullable(UInt32)`, `Nullable(String)`, `Nullable(FixedString(16))`, `Nullable(DateTime('UTC'))`.

Al igual que los demás tipos compuestos, `Nullable` delega la [fase de prefijo](#composite-types) en su serialización interna antes de escribir el mapa de nulos: cuando el tipo interno es versionado, su prefijo de estado se emite **primero**. Por tanto, `Nullable(Tuple(LowCardinality(String)))` comienza con el prefijo de estado de `LowCardinality`, no con el mapa de nulos. Cuando el tipo interno es un tipo hoja u otro tipo no versionado, la fase de prefijo no emite ningún byte.

La disposición en formato wire consta de la fase de prefijo interna (vacía, salvo que el tipo interno sea versionado), seguida de dos flujos concatenados, empezando por el 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
```

El mapa de nulos ocupa exactamente `num_rows` bytes, uno por fila:

| Valor del byte | Significado |
| - | - |
| `0x00` | Hay un valor en esta fila. |
| distinto de cero (canónico `0x01`) | El valor es NULL. Los bytes correspondientes en el flujo de valores son de relleno (placeholder). |

El flujo de valores contiene la codificación estándar del tipo interno para **todas** las `num_rows` filas, incluidas las posiciones nulas. Un decodificador debe leer igualmente los bytes de relleno de las posiciones nulas para avanzar en el flujo, pero debe consultar el mapa de nulos antes de interpretar cualquier valor individual. Los emisores pueden escribir cualquier byte en las posiciones nulas, por lo que los decodificadores no deben depender de un valor de relleno concreto.

Valores de relleno según la familia del tipo interno:

| Familia del tipo interno | Relleno en una posición nula |
| - | - |
| Ancho fijo (UInt/Int/Float/DateTime/UUID/etc.) | Bytes inicializados a cero con el ancho del tipo |
| `String` | Cadena vacía: un único byte `0x00` |
| `FixedString(N)` | `N` bytes a cero |
| `Array(T)` | Array vacío: los desplazamientos avanzan cero |
| `Tuple(T1, T2, ...)` | Cada elemento usa su propio valor de relleno |

`Nullable(T)` puede aparecer dentro de `Array`, `Tuple`, `Map` y `Nested`; `Array(Nullable(T))` y `Tuple(Nullable(T1), T2)` son habituales. La nulabilidad no puede anidarse en sí misma: el servidor rechaza `Nullable(Nullable(T))`.

Un `Nullable(UInt8)` con tres filas `[5, NULL, 9]` (6 bytes en total):

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

Un `Nullable(String)` con tres filas `["hello", NULL, "world"]` (15 bytes en 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)

Cadena de tipo: `Array(InnerType)`. Ejemplos: `Array(UInt32)`, `Array(String)`, `Array(Nullable(UInt32))`, `Array(Array(UInt8))`.

La disposición en formato wire consta de la [fase de prefijo](#composite-types) del tipo interno (vacía salvo que el tipo interno esté versionado), seguida de dos flujos concatenados, empezando por los desplazamientos:

```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
```

El flujo de desplazamientos (offsets stream) contiene exactamente `num_rows` valores UInt64 en formato little-endian, cada uno de los cuales indica la **posición final acumulada** en el flujo de valores (values stream) tras los elementos de esa fila:

* Índice inicial de los elementos de la fila `N` = `offsets[N - 1]` (o `0` cuando `N == 0`).
* Índice final de los elementos (exclusivo) de la fila `N` = `offsets[N]`.
* Número de elementos de la fila `N` = `offsets[N] - offsets[N - 1]`.

Por lo tanto, `offsets[num_rows - 1]` es el número total de elementos de todas las filas, y el flujo de valores contiene esa misma cantidad de valores internos concatenados uno tras otro.

Los desplazamientos son **monótonamente no decrecientes**: dos desplazamientos consecutivos iguales indican una fila vacía, y un decodificador debe rechazar los desplazamientos no monótonos por considerarlos datos corruptos. Una columna vacía (`num_rows == 0`) escribe cero bytes, es decir, no hay flujo de desplazamientos ni flujo de valores. Los tipos internos pueden ser de cualquier tipo, incluidos otros tipos compuestos: `Array(Array(T))`, `Array(Tuple(...))` y `Array(Nullable(T))` son todos válidos.

`Array(UInt32)` con las filas `[[10, 20, 30], [], [40, 50]]` (44 bytes en 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 desplazamiento es el *final* acumulado del segmento que corresponde a una fila dentro del flujo de valores compartido; el inicio es el desplazamiento anterior (o `0` para la fila 0). Dos desplazamientos consecutivos iguales indican una fila vacía:

![Desplazamientos que asignan filas a un flujo de valores compartido](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)` con las filas `[["a", "bb"], []]` (20 bytes en 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))` con las filas `[[[1,2]], [], [[3], [4,5]]]` anida la misma estructura:

* Desplazamientos externos: `[1, 1, 3]`: la fila 0 tiene 1 array interno, la fila 1 tiene 0 y la fila 2 tiene 2.
* El `Array(UInt32)` intermedio decodifica 3 filas con desplazamientos `[2, 3, 5]`.
* El `UInt32` más interno decodifica 5 valores: `[1, 2, 3, 4, 5]`.

En total: 24 (desplazamientos externos) + 24 (desplazamientos intermedios) + 20 (valores) = 68 bytes.

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

Cadena de tipo: `Tuple(T1, T2, ..., Tn)`. Ejemplos: `Tuple(UInt32, String)`, `Tuple(Int32)`, `Tuple(Array(UInt32), String)`, `Tuple(UInt8, Tuple(Int32, String))`. ClickHouse también admite **tuplas con nombre** mediante `Tuple(a UInt32, b String)`; los nombres son solo metadata y no afectan al formato wire.

La disposición en formato wire consta de la [fase de prefijo](#composite-types) de los elementos (cada elemento versionado aporta su prefijo de estado, en orden de declaración; los elementos no versionados no aportan nada), seguida de *N* flujos concatenados, uno por cada tipo de elemento, en orden de declaración:

```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 flujo codifica exactamente `num_rows` valores. No hay prefijo de longitud, ni flujo de desplazamientos, ni separadores entre flujos. Una columna vacía (`num_rows == 0`) escribe cero bytes por flujo. Los tipos de los elementos pueden ser cualquiera, incluidos otros tipos compuestos: `Tuple(Tuple(...), ...)`, `Tuple(Array(...), ...)` y `Tuple(Nullable(T1), T2)` son todos válidos.

La tupla sin elementos `Tuple()` también es válida; se genera a partir de expresiones como `SELECT tuple()` o `CAST(x AS Tuple())`. Como no tiene flujos de elementos, se serializa como [Nothing](#nothing): **un byte de relleno (`0x30`, ASCII `'0'`) por fila**, que el deserializador descarta. El número de filas se obtiene del encabezado del bloque, exactamente igual que con `Nothing`.

`Tuple(UInt8, UInt8)` con 3 filas `(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
```

La disposición **no** es por filas (row-major): al volver a leer los bytes sin procesar se obtiene `[1, 2, 3]` para el elemento 0 y `[4, 5, 6]` para el elemento 1.

`Tuple(UInt32, String)` con 2 filas `(10, "a")`, `(20, "bb")` (13 bytes en 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)

Cadena de tipo: `Map(KeyType, ValueType)`. Ejemplos: `Map(String, UInt32)`, `Map(String, Array(UInt32))`, `Map(UInt8, Tuple(Int32, String))`, `Map(Array(String), Int8)`. El formato wire no impone ninguna restricción a ninguno de los dos tipos: tanto `K` como `V` pueden ser cualquier tipo compatible, incluidos los tipos compuestos. (Las reglas de ClickHouse a nivel de SQL sobre los tipos de clave admitidos han variado de una versión a otra; consulte la documentación de SQL correspondiente a la versión del servidor de destino).

La disposición en formato wire es idéntica byte a byte a la de `Array(Tuple(K, V))`, por lo que comienza con la [fase de prefijo](#composite-types) interna (vacía, salvo que `K` o `V` sean versionados):

```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
```

donde `total_pairs = offsets[num_rows - 1]` (o `0` cuando `num_rows == 0`). El flujo de desplazamientos tiene la misma semántica que en [Array](#array). Las claves están alineadas posicionalmente con los valores: el par `i` es `(keys[i], values[i])`.

En memoria, ClickHouse representa una columna Map como un array de tuplas; el sistema de tipos la expone como un tipo independiente para facilitar su uso en SQL (`m['key']`, `mapKeys`, `mapValues`). El formato wire es una serialización directa de ese almacenamiento, por lo que `Map` y `Array(Tuple(K, V))` son intercambiables byte a byte.

Los desplazamientos son monótonos no decrecientes, y tanto el flujo de claves como el de valores contienen exactamente `total_pairs` valores. Una columna vacía no escribe ningún byte. Dentro de una misma fila, las claves suelen ser únicas, pero se trata de una regla semántica, no de una restricción impuesta por el formato wire: este permite que las claves duplicadas se escriban y se vuelvan a leer sin alteraciones, y la semántica del lado del servidor resuelve los duplicados solo cuando una función compatible con Map procesa la fila.

`Map(UInt8, UInt8)` con 2 filas `{1:10, 2:20}`, `{3:30}` (22 bytes en 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
```

Las claves y los valores se almacenan como flujos independientes, sin intercalarse: el par `i` se reconstruye leyendo `keys[i]` y `values[i]` a la vez.

`Map(String, UInt32)` con 1 fila `{'a':1, 'b':2}` (20 bytes en 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, ...)

La representación en el wire de `Nested` depende de la configuración `flatten_nested` del lado del servidor, lo que da lugar a dos casos distintos.

![Disposición de columnas compuestas con y sin aplanamiento](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` (valor predeterminado del servidor).** Si la tabla se creó con la configuración predeterminada, `Nested` **no es un tipo wire**. El servidor almacena y expone la columna como N columnas `Array(T_i)` paralelas con **nombres separados por puntos** (`outer.field1`, `outer.field2`, etc.). Para la capa de formato no hay nada nuevo: cada una de estas columnas es un [Array](#array) normal:

```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`.** Si la tabla se creó con `flatten_nested = 0`, la columna aparece en el wire como una única columna con la cadena de tipo `Nested(name1 T1, name2 T2, ...)`, y su disposición tras la cadena de tipo es **idéntica byte a byte a `Array(Tuple(T1, T2, ..., Tn))`**, incluida la [fase de prefijo](#composite-types) interna, de modo que cualquier campo versionado `T_i` emite primero su prefijo de estado, antes de los desplazamientos. El siguiente ejemplo utiliza campos no versionados, por lo que la fase de prefijo está vacía:

```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
```

La única diferencia está en el texto de la cadena de tipo: `Nested` conserva los nombres de los campos (`a`, `b`), mientras que `Array(Tuple)` no los incluye como posiciones con nombre.

La cadena de tipo del caso B es una lista de pares (nombre, tipo) separados por comas. El primer espacio en blanco separa el nombre de su tipo; el tipo en sí puede contener más espacios en blanco, comas y paréntesis, por lo que, para analizarla, se necesita el mismo divisor sensible a la profundidad que se utiliza para `Tuple`. La disposición en formato 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    ┘
```

donde `total_elements = offsets[num_rows - 1]` (o `0` cuando `num_rows == 0`). Los desplazamientos son monótonos no decrecientes, y cada flujo de campo contiene exactamente `total_elements` valores. En el momento del INSERT, el servidor exige que, dentro de una misma fila, todos los campos tengan el mismo número de elementos. Una columna vacía no escribe ningún byte.

`Nested(a UInt8, b String)` con 2 filas, `[(10,'x'),(20,'y')]` y `[(30,'z')]` (25 bytes después de la cadena de 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"
```

### Alias de tipos

Varios tipos son simples alias: el servidor envía el nombre del alias en el encabezado de la columna, pero los bytes que siguen corresponden a un tipo subyacente. Un decodificador asigna el alias a ese tipo y reutiliza su códec; no interviene ningún formato wire nuevo.

Los tipos geográficos son alias de arrays y tuplas anidados:

| Cadena de tipo | Tipo wire subyacente |
| - | - |
| `Point` | `Tuple(Float64, Float64)` |
| `Ring`, `LineString`, `MultiPoint` | `Array(Point)` |
| `Polygon`, `MultiLineString` | `Array(Ring)` |
| `MultiPolygon` | `Array(Polygon)` |

Así, una columna `Point` se decodifica exactamente como `Tuple(Float64, Float64)` (y se representa como `(1,2)`), una `Ring` como `Array(Tuple(Float64, Float64))` (`[(0,0),(1,1)]`), y así sucesivamente a lo largo de la jerarquía.

`Geometry` también es un alias, pero de un [`Variant`](#variant) en lugar de un array anidado: su carga útil es la variante de los siete tipos geográficos anteriores. El encabezado de la columna contiene únicamente la cadena de tipo `Geometry` —**no** desglosa la variante—, por lo que el decodificador debe expandirla por su cuenta. A diferencia de un `Variant` definido por el usuario, los discriminadores de `Geometry` **no** siguen el orden alfabético por nombre: son fijos por motivos de compatibilidad, y los nuevos tipos geográficos siempre se añaden al final. La correspondencia es `0` = `LineString`, `1` = `MultiLineString`, `2` = `MultiPolygon`, `3` = `Point`, `4` = `Polygon`, `5` = `Ring`, `6` = `MultiPoint` (añadido en 26.7; los flujos procedentes de servidores más antiguos simplemente nunca contienen el discriminador `6`). A continuación, cada valor seleccionado se decodifica mediante su alias geográfico correspondiente de la tabla anterior (`NULL` usa el discriminador `NULL` de `Variant`, `255`).

`SimpleAggregateFunction(func, T)` es un alias de su tipo de valor `T`. Almacena un valor de agregación ya finalizado, por lo que su forma wire y su representación son exactamente las de `T` (`SimpleAggregateFunction(sum, UInt64)` se decodifica como `UInt64`). Solo la forma con un único tipo de valor funciona como alias de este modo; el tipo subyacente puede ser, a su vez, un tipo compuesto.

> [!NOTE]
>
> Dos tipos relacionados **no** son alias. Son tipos de columna `Native` válidos —por ejemplo, un cliente puede recibir una columna `AggregateFunction` procedente de un combinador `-State` o de una agregación distribuida—, pero cada uno lleva su propia carga útil especializada, que queda fuera del alcance de esta página:
>
> * `AggregateFunction(func, ...)` contiene un estado de agregación *intermedio* (no un valor final); su disposición binaria depende de la función de agregación y de su versión. El nombre de la función puede incluir parámetros, cada uno impreso como un literal SQL, como en `AggregateFunction(topK(3), UInt64)`. Algunas funciones de agregación imprimen un sufijo explícito `::Type` en un parámetro, como en `AggregateFunction(topK('3'::Decimal32(0)), UInt64)`, porque un valor `Decimal` o de entero ancho no tiene una forma literal simple que imprimir; que una función lo haga depende de esa función y no del formato, por lo que un decodificador que inspeccione los parámetros debe aceptar ambas grafías. Un decodificador que solo necesite los tipos de los argumentos no se ve afectado. Una función de agregación versionada escribe explícitamente una versión de estado distinta de cero como primer argumento de la cadena de tipo —`AggregateFunction(1, quantileDeterministic, UInt64, UInt64)`—, mientras que la versión `0` no se imprime, de modo que la cadena de tipo sigue siendo `AggregateFunction(quantileDeterministic, UInt64, UInt64)`. En una conexión negociada (revisión superior a `0`), el emisor elige la versión **únicamente** en función de la revisión del protocolo —nunca de una versión que esté fijada en su tipo local, porque en el wire la versión `0` no se distingue de «ninguna versión anunciada»—, por lo que la misma función puede anunciar versiones distintas en conexiones distintas, y un lector debe tomar la versión de la cadena de tipo en lugar de dar por hecho que es la más reciente. En la revisión `0` no hay ningún par para el que derivar una versión —el stream es autodescriptivo y lo vuelve a leer quien lo escribió—, por lo que una versión fijada en el tipo se conserva en el stream (y en la cadena de tipo anunciada) en lugar de rebajarse a `0`. `quantileDeterministic` (junto con las grafías `quantilesDeterministic` y `medianDeterministic`) está versionada a partir de la revisión `54491`: la versión `1` añade al final del estado un `UInt8 skip_degree`, y su prefijo es idéntico byte a byte al de la versión `0`. Por debajo de esa revisión, el emisor genera estados de versión `0`, por lo que un par más antiguo no se ve afectado. `uniq` está versionada a partir de la revisión `54493`: la versión `1` antepone un byte de indicadores a la disposición de la versión `0` y, cuando el bit `0` de los indicadores está activado, añade al final una pequeña muestra adicional de hashes completos de 64 bits, cada uno combinado mediante XOR con la constante `0x9E3779B97F4A0000` (`UInt8 skip_degree`, `VarUInt count` y, a continuación, los valores de 8 bytes), lo que mantiene la precisión de la estimación con cardinalidades superiores a dos mil millones; un estado de versión `0` solo incluye la muestra principal de 32 bits. El bit `1` de los indicadores señala que la muestra de 64 bits está *incompleta* —el estado ha absorbido un estado de versión `0`, cuyos elementos nunca se ofrecieron a una muestra de 64 bits—, en cuyo caso no se escribe ninguna muestra de 64 bits y un lector debe realizar la estimación únicamente a partir de la muestra principal de 32 bits (y seguir indicándolo en los estados que escriba después); por tanto, los dos bits son mutuamente excluyentes, y un lector debe rechazar un estado que active ambos. No se define ningún otro bit, y un lector debe rechazar un estado que active alguno. Todo esto se aplica igualmente a un estado anidado dentro de un tipo contenedor —`Array`, `Tuple`, `Map`, `Nullable`, `Variant` (donde la versión explícita de la alternativa cambia la cadena de tipo sin alterar el orden del discriminador), o un envoltorio `Nested` / `SimpleAggregateFunction`—. También se aplica a los combinadores de funciones de agregación: un combinador transparente sobre una función versionada —`uniqIf`, `uniqArray`, `uniqForEach`, `uniqMap`, `uniqArgMin` / `uniqArgMax`, `uniqOrNull` / `uniqOrDefault`, `uniqResample` y el adaptador implícito para argumentos `Nullable`— transmite la versión a los estados anidados que contiene su carga útil, escribe explícitamente la versión actual de la función anidada en la cadena de tipo de un estado recién creado, exactamente igual que la función simple (`AggregateFunction(1, uniqIf, UInt64, UInt8)`), y, en una conexión negociada, deriva la versión de la revisión del protocolo de la misma manera. El nombre personalizado de un envoltorio contiene una segunda copia de su tipo de argumento, por lo que el emisor debe reconstruir esa copia con la misma versión negociada del estado de agregación; de lo contrario, su encabezado podría anunciar la versión `1` para una carga útil de versión `0`. Hay un caso límite de `Variant` que no tiene ninguna representación en el wire: cuando dos alternativas solo difieren en una versión de estado explícita (algo válido como tipo de columna, ya que las cadenas de tipo son distintas), volver a versionar ambas para la revisión negociada las fusionaría en una misma alternativa, por lo que el emisor debe hacer fallar la transferencia en lugar de anunciar versiones que no corresponden a las de la carga útil. `Dynamic` es la excepción deliberada al versionado derivado de la revisión: una columna `Dynamic` anuncia por sí misma sus tipos anidados —mediante sus propias listas de tipos y mediante la [codificación binaria de tipos](https://clickhouse.com/docs/es/reference/data-types/data-types-binary-encoding.md), cuyo campo de versión explícito transporta la versión de estado explícita (los valores de su variante compartida incrustan así su tipo incluso en reposo)—, y esos anuncios forman parte de los propios datos de la columna, idénticos para cualquier par y cualquier medio, por lo que nunca se reescriben para una revisión negociada. Por tanto, un estado anidado dentro de un valor `Dynamic` se escribe con la versión indicada en su tipo (que todo estado recién creado incluye, ya que el resultado de una consulta `-State` escribe su versión explícitamente), o con la versión `0` cuando el tipo no indica ninguna, en todas las conexiones y medios por igual.
> * `QBit(T, N[, stride])` almacena un vector con sus planos de bits transpuestos para cargas de trabajo de búsqueda vectorial; la disposición de su flujo en el wire (flujos de planos de bits `FixedString` ordenados por grupo, `element_size * (N / stride)` de ellos con un `stride` explícito) y su codificación binaria de tipos (etiqueta `0x36`, o `0x37` `QBitWithStride` cuando `stride != N`) se documentan en la [página del tipo de datos `QBit`](https://clickhouse.com/docs/es/reference/data-types/qbit) y en la referencia de la [codificación binaria de tipos](https://clickhouse.com/docs/es/reference/data-types/data-types-binary-encoding.md), de modo que un lector de `Native` no tiene que deducirlas a partir del código fuente en C++.

### Tipos versionados

Los tipos versionados incluyen, en su representación wire, un prefijo de versión de serialización que indica qué variante de la codificación viene a continuación. También pueden usar múltiples flujos (como los tipos compuestos). En el formato wire `Native`, el prefijo y cualquier diccionario son por bloque: estos tipos no mantienen ningún estado entre bloques (consulte [la nota sobre el prefijo por bloque](#serialization-version-concept) más abajo); el estado de serialización entre bloques solo existe en el flujo en disco de MergeTree.

Estos tipos son bastante más complejos que los tipos compuestos de forma fija, por lo que un Client orientado a consultas analíticas sencillas puede dejar su implementación para más adelante.

#### Versión de serialización: concepto

Una **versión de serialización** es un número de versión on-wire, por tipo y por columna, que indica qué variante de la codificación de un tipo utiliza el emisor. Es lo primero que aparece en el prefijo de estado de la columna, de modo que el decodificador la lee y delega en el analizador adecuado para el resto de la columna.

No debe confundirse con la versión del protocolo:

| Dimensión | Versión del protocolo | Versión de serialización (esta sección) |
| - | - | - |
| Ámbito | Toda la conexión | Por tipo, por columna |
| Negociada | Sí, durante el handshake | No — el emisor escribe, el receptor lee |
| Controla | Qué funcionalidades a nivel de paquete están activas | Qué variante wire de un tipo |
| Lectura obligatoria | Sí | Sí, para cada columna versionada |

La mayoría de los tipos versionados escriben la versión como un UInt64 little-endian justo antes de cualquier otro dato del prefijo de estado; unos pocos usan VarUInt o UInt8. Un decodificador lee primero la versión y rechaza los valores desconocidos: una versión superior implica un formato de emisor más reciente que el decodificador no entiende, y analizarlo incorrectamente corrompe todos los bytes posteriores.

El prefijo de estado se emite al comienzo de **cada bloque cuyo número de filas es mayor que cero**, justo antes de la carga útil de ese bloque.

El escritor y el lector Native **no** conservan el estado de serialización entre bloques: `NativeWriter` crea un estado de serialización nuevo y escribe un prefijo de estado por cada bloque de columna no vacío que escribe, y `NativeReader` crea un estado de deserialización nuevo y lo lee por cada bloque no vacío que lee (ambos omiten por completo el prefijo cuando `rows == 0`).

Por lo tanto, los bloques de encabezado (rows = 0) y los bloques vacíos no emiten nada, y un decodificador debe volver a leer el prefijo de estado al comienzo de cada bloque no vacío. Un decodificador que lea el prefijo una sola vez y trate los bloques posteriores como si solo contuvieran carga útil interpretará el prefijo del siguiente bloque como datos y perderá la sincronización:

![Prefijo de estado repetido en cada bloque no vacío](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)

#### Referencia de versiones de serialización

| Tipo | Ancho del campo | Valor | Nombre | Significado |
| - | - | - | - | - |
| **Object** (base para JSON) | UInt64 LE | `0` | `V1` | Codificación original. Incluye el parámetro `max_dynamic_paths` y una lista de rutas dinámicas. |
| | | `1` | `STRING` | Modo de compatibilidad del formato Native: el Object se transmite como una única columna `String` que contiene texto JSON. |
| | | `2` | `V2` | Disposición de V1 sin el parámetro `max_dynamic_paths`. |
| | | `3` | `FLATTENED` | Modo de compatibilidad del formato Native: representación aplanada de rutas. |
| | | `4` | `V3` | Se escribe solo en partes de MergeTree, nunca en el wire; su disposición no se especifica aquí. |
| **Dynamic** | UInt64 LE | `1` | `V1` | Codificación original. Incluye `max_dynamic_types` y una lista de tipos Variant en tiempo de ejecución. |
| | | `2` | `V2` | V1 sin el parámetro `max_dynamic_types`. |
| | | `3` | `FLATTENED` | Modo de compatibilidad del formato Native. |
| | | `4` | `V3` | Se escribe solo en partes de MergeTree, nunca en el wire; su disposición no se especifica aquí. |
| Modo de discriminadores de **Variant** | UInt64 LE | `0` | `BASIC` | El discriminador de cada fila se escribe de forma literal. |
| | | `1` | `COMPACT` | Si todas las filas de un gránulo comparten el mismo discriminador, solo se escribe un único valor junto con un marcador de gránulo. |
| Formato de gránulo de **Variant** (cuando el modo es `COMPACT`) | UInt8 | `0` | `PLAIN` | El gránulo contiene discriminadores heterogéneos. |
| | | `1` | `COMPACT` | El gránulo tiene un único discriminador para todas las filas. |
| Serialización de claves de **LowCardinality** | Int64 | `1` | `sharedDictionariesWithAdditionalKeys` | Única versión definida actualmente. |
| Alternativa **JSON-as-String** (cuando `output_format_native_write_json_as_string` está habilitado) | UInt64 LE | `1` | `JSONStringSerializationVersion` | La columna JSON llega como una columna `String` precedida por este prefijo. |

Algunos aspectos de la tabla que conviene destacar:

* **Los valores no son contiguos.** `Dynamic` usa `1`, `2`, `3` y `4`, con `V3` en `4` y `FLATTENED` en `3`. Un número mayor no implica necesariamente una versión más reciente.
* **Algunos valores son exclusivos del formato Native.** `Object::STRING`, `Object::FLATTENED` y `Dynamic::FLATTENED` existen para mantener la compatibilidad del protocolo nativo con los Clients que no implementan Object/Dynamic por completo. No aparecen en el almacenamiento en disco de MergeTree.
* **`V3` es exclusivo del almacenamiento en disco.** `Object::V3` y `Dynamic::V3` figuran en la tabla porque esos valores ya están asignados y no deben reutilizarse, no porque un decodificador pueda encontrárselos: ClickHouse nunca los envía a través de `Native` y, en ese contexto, los rechaza con `INCORRECT_DATA`. Todo lo que contienen —la subversión de shared-data de `Object::V3` y sus disposiciones de buckets y gránulos— describe una parte y no un bloque, por lo que queda fuera del alcance de esta página. Los Clients que consumen el protocolo TCP nativo solo ven `V1`, `V2`, `STRING` o `FLATTENED`.

#### LowCardinality(T)

El tipo versionado más sencillo. Sustituye una columna de `N` valores internos por un pequeño diccionario de valores únicos y `N` índices que apuntan a ese diccionario.

Cadena de tipo: `LowCardinality(InnerType)`. Ejemplos: `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
```

El prefijo de estado (Int64 LE = 1) es la única versión definida, `sharedDictionariesWithAdditionalKeys`; los demás valores están reservados.

El UInt64 de metadata por bloque es un campo de bits:

| Rango de bits | Significado |
| - | - |
| 0..7 | Código del tipo de clave: `0` = UInt8, `1` = UInt16, `2` = UInt32, `3` = UInt64. Se elige el tipo más pequeño capaz de indexar `dict_size` entradas. |
| 8 (`0x100`) | `NeedGlobalDictionaryBit`: un único diccionario compartido entre bloques. **Nunca se activa en el formato `Native`**: el escritor Native usa `low_cardinality_max_dictionary_size = 0` y el lector Native rechaza este bit (`native_format` lanza `INCORRECT_DATA` — "cannot use global dictionary"). Corresponde al stream en disco de MergeTree, no al wire. |
| 9 (`0x200`) | `HasAdditionalKeysBit`: se activa cuando el bloque incluye claves de diccionario adicionales (escritas antes de los índices). Siempre está activado en un bloque `Native` no vacío. |
| 10 (`0x400`) | `NeedUpdateDictionary`: se activa cuando el bloque incluye una actualización del diccionario. Siempre está activado en un bloque `Native` no vacío, ya que cada bloque envía su propio diccionario autocontenido. |

En una respuesta de consulta típica con un único bloque de datos por columna, la metadata es `0x600` (HasAdditionalKeys + NeedUpdateDictionary).

Los valores del dict son `dict_size` valores codificados con el tipo interno T. El diccionario reserva las primeras posiciones para valores especiales: una columna no Nullable reserva una (`dict[0]` contiene el valor predeterminado del tipo interno, p. ej., `""` para `String`), y los valores distintos reales empiezan en `dict[1]`.

En `LowCardinality(Nullable(T))`, el dict se sigue codificando como T simple (sin stream de mapa de nulos), pero se reservan **dos** posiciones: `dict[0]` es el marcador NULL y `dict[1]` es el valor predeterminado del tipo interno (p. ej., `""` para `String`); los valores distintos reales empiezan en `dict[2]`. La clave de una fila NULL apunta a `dict[0]`, y esa posición se escribe en el wire con los bytes predeterminados del tipo interno.

Las claves son índices del dict; cada índice ocupa `1 << key_type_code` bytes (1, 2, 4 u 8), y el valor `N` se reconstruye como `dict[keys[N]]`.

`keys_count` es el número de valores `LowCardinality` en el **nivel recursivo actual**, que no tiene por qué coincidir con el número de filas del bloque. En una columna `LowCardinality` de nivel superior, ambos coinciden. Pero cuando `LowCardinality` está anidado dentro de un tipo compuesto, el recuento es el número aplanado de valores que le transmite el tipo compuesto: en `Array(LowCardinality(String))` con tres filas que contienen cinco elementos en total, `keys_count` es `5`, no `3`; en `Map(K, LowCardinality(V))` es el número total de pares, y así sucesivamente. Un decodificador debe obtener `keys_count` de este campo en lugar de suponer que equivale al número de filas del bloque. Cuando ese recuento aplanado es cero (por ejemplo, en un bloque cuyos arrays están todos vacíos), la fase de datos de `LowCardinality` no escribe **absolutamente nada**: solo está presente el prefijo de estado (emitido en la [fase de prefijo del tipo compuesto](#composite-types)), sin metadata, diccionario ni `keys_count` a continuación.

El prefijo de estado se lee al inicio de cada bloque cuyo número de filas es mayor que cero; los bloques de encabezado (rows = 0) y los bloques vacíos no emiten nada. Dentro de un bloque, `keys_count` es igual al número de filas, `dict_size` es igual al número de valores del stream del diccionario, y cada clave ocupa `1 << key_type_code` bytes.

> [!NOTE]
>
> En el formato `Native`, cada bloque incluye su propio **diccionario autocontenido y local al bloque**: no se comparte estado de diccionario entre bloques. El escritor Native establece `low_cardinality_max_dictionary_size = 0`, de modo que `SerializationLowCardinality` nunca construye un diccionario compartido: cada bloque no vacío escribe sus claves como claves adicionales locales al bloque con `NeedGlobalDictionaryBit` desactivado (metadata `0x600`), y el lector Native rechaza `NeedGlobalDictionaryBit` cuando `native_format` es true. Por lo tanto, un decodificador debe restablecer el diccionario en cada bloque y leer las `dict_size` entradas presentes en ese bloque; reutilizar el diccionario de un bloque anterior haría que las claves del siguiente bloque se leyeran incorrectamente. (Persistir un diccionario LC entre bloques es una cuestión del almacenamiento en disco de MergeTree, no de la disposición en formato wire de Native).

`LowCardinality(String)` con los 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
```

Reconstruido: `dict[1], dict[2], dict[1], dict[3], dict[2]` = `["a", "b", "a", "c", "b"]`.

`LowCardinality(Nullable(String))` con los valores `['a', NULL, '', 'b']` muestra las dos posiciones reservadas: `dict[0]` para NULL y `dict[1]` para la cadena vacía como valor predeterminado:

```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
```

Reconstruido: `dict[2]` = `"a"`, `dict[0]` = `NULL`, `dict[1]` = `""`, `dict[3]` = `"b"`, es decir, `["a", NULL, "", "b"]`. Tanto `dict[0]` como `dict[1]` son bytes vacíos en el wire; la nulidad se debe a que la clave apunta a la posición `0`, no a los bytes.

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

El tipo `JSON` de ClickHouse tiene varias codificaciones wire (consulte [la referencia de versiones de serialización](#serialization-version-reference)). Tier 1 es la más sencilla: cuando se establece el ajuste de consulta `output_format_native_write_json_as_string = 1`, el servidor convierte cada valor JSON en su texto serializado y emite la columna como `String` con un marcador de prefijo de estado.

Cadena de 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]
```

El valor del prefijo de estado es `1` para este String fallback. Los demás valores indican distintas codificaciones de `JSON`/`Object`: `0` = V1, `2` = V2 (la predeterminada en el protocolo TCP nativo), `3` = FLATTENED, `4` = V3 (consulte [la referencia de versiones de serialización](#serialization-version-reference)). Si un decodificador encuentra aquí un valor distinto de `1`, no se trata del String fallback. El prefijo se lee al inicio de cada bloque con filas > 0, y el stream de valores es una columna [String](#string-type) estándar de `num_rows` filas.

Valor `JSON` `'{"a":1}'` (una fila):

```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}
```

El valor se emite como su texto JSON compacto (`{"a":1}`), y el integer se mantiene como integer. El texto es simplemente un valor `String`, por lo que el Client recibe el JSON para transportarlo de forma opaca, pero no recupera las rutas individuales ni sus tipos de ClickHouse; para obtener un tipado fiel por ruta se necesita la codificación Tier 2 que se describe a continuación.

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

Una unión discriminada: cada fila contiene un valor de exactamente uno de los tipos Variant, o NULL. Cada fila lleva un **discriminador global** de un byte que selecciona su tipo, y los valores de cada tipo se almacenan después de forma compacta, en un bloque contiguo por cada tipo Variant.

Cadena de tipo: `Variant(T1, T2, ...)`. El servidor normaliza el orden (los tipos Variant se ordenan por nombre), por lo que la cadena de tipo recibida ya enumera los tipos en **orden de discriminador global**: el discriminador `0` selecciona el primer tipo de la lista, `1` el segundo, y así sucesivamente. `255` (`NULL_DISCRIMINATOR`) indica que la fila es NULL. Los elementos de Variant nunca son `Nullable`: representar NULL es tarea del discriminador. Ejemplos: `Variant(String, UInt64)`, `Variant(Array(UInt8), String)`.

El prefijo de estado contiene un modo de discriminadores `UInt64 LE`: `0` = BASIC (el discriminador de cada fila se escribe literalmente), `1` = COMPACT (codificación run-length por gránulo). De forma predeterminada, el servidor utiliza BASIC en el protocolo nativo (`use_compact_variant_discriminators_serialization = false`); aquí solo se especifica BASIC.

```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 los datos, recorra los discriminadores de izquierda a derecha manteniendo un contador acumulado por tipo. La fila `r` con discriminador `d` (≠ 255) toma el valor situado en el índice `counter[d]` de la secuencia de valores del tipo Variant `d` y, a continuación, se incrementa `counter[d]`. Las filas con discriminador `255` son NULL y no consumen ningún valor de ninguna secuencia, por lo que la suma de los contadores por tipo es igual al número de filas no NULL.

El prefijo de estado (el modo `UInt64`) se lee al inicio de cada bloque con más de 0 filas; los bloques de encabezado y los bloques vacíos no emiten nada. Cada discriminador no NULL es menor que el número de tipos Variant, y el tipo Variant `i` se decodifica para exactamente `count[i]` filas.

> [!NOTE]
>
> Los elementos de Variant que, a su vez, tienen estado (`LowCardinality`, `Variant`, `Dynamic`, `JSON`) emiten su propio prefijo de estado en la fase de prefijo de estado por elemento, después del modo `UInt64`. Los tipos hoja y los tipos compuestos simples (`Array`, `Tuple`, `Map` de tipos hoja) tienen prefijos de estado vacíos y pueden componerse libremente.

`Variant(String, UInt64)` con los valores `[42, 'hi', NULL]` (el orden canónico sitúa `String` antes que `UInt64`, por lo que el discriminador 0 = String y 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
```

Reconstrucción: fila 0 = UInt64 run\[0] = `42`; fila 1 = String run\[0] = `"hi"`; fila 2 = NULL.

El stream de discriminadores actúa como índice: cada discriminador no NULL toma el siguiente valor del bloque denso de su tipo, mientras que `255` (NULL) no consume nada. Este mismo recorrido permite reconstruir [Dynamic](#dynamic), que solo se diferencia en la forma de codificar NULL:

![Discriminadores que seleccionan valores de bloques densos específicos 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

Una columna cuyo tipo de valor se determina en tiempo de ejecución: cada fila contiene un valor de uno de los tipos de un conjunto determinado en tiempo de ejecución, o NULL. A diferencia de `Variant`, el conjunto de tipos **no** figura en la cadena de tipo de la columna, sino que se transmite en el prefijo de estado.

Cadena de tipo: `Dynamic` o `Dynamic(max_types=N)`. El parámetro `max_types` limita cuántos tipos distintos registra la columna, pero no afecta al formato wire que se describe a continuación.

`Dynamic` tiene cuatro codificaciones: `V1 = 1`, `V2 = 2`, `FLATTENED = 3`, `V3 = 4`. La que emite el servidor depende del canal y de los ajustes de consulta:

* A través de `clickhouse-client` y de HTTP `FORMAT Native`, la revisión del escritor es `0` (salvo que se aumente con `client_protocol_version`), por lo que la codificación predeterminada es **V1**.
* A través del protocolo TCP nativo, con su revisión negociada, la codificación predeterminada es **V2**. El escritor `Native` mantiene las estadísticas deshabilitadas, por lo que una carga útil `V2` predeterminada no incluye estadísticas por variante: tras la lista de tipos aparecen directamente el prefijo y los datos del `Variant` anidado. (Las estadísticas por variante atañen al almacenamiento en disco de MergeTree y no forman parte del formato wire Native).
* El ajuste de consulta `output_format_native_use_flattened_dynamic_and_json_serialization = 1` prevalece sobre ambos casos y emite **FLATTENED (versión 3)** independientemente de la revisión.

> [!NOTE]
>
> **Alcance**
>
> Esta página especifica únicamente la disposición **`FLATTENED`**. Las disposiciones binarias no aplanadas `V1`/`V2`/`V3` son la representación interna/en disco (listas de tipos codificadas en binario, estadísticas por variante) y **no** se especifican aquí. Un Client que quiera decodificar `Dynamic` siguiendo esta página debe solicitar `FLATTENED` estableciendo `output_format_native_use_flattened_dynamic_and_json_serialization = 1`; la disposición siguiente presupone ese ajuste. Dado que el byte de versión encabeza el prefijo, un decodificador puede detectar qué codificación ha recibido realmente y rechazar `V1`/`V2`/`V3` si solo implementa `FLATTENED`.

La disposición **FLATTENED (versión 3)** que se selecciona con ese ajuste:

```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
```

El ancho del discriminador es el entero sin signo más pequeño capaz de indexar `num_types` tipos más la posición reservada para NULL: `UInt8` para `num_types ≤ 255` y, a continuación, `UInt16`, `UInt32` y `UInt64`. NULL se representa con el valor de discriminador igual al propio `num_types`, a diferencia de `Variant`, donde NULL es el valor fijo `255`. La reconstrucción sigue el mismo recorrido denso que en `Variant`: se mantiene un contador por tipo, y la fila `r` con discriminador `d` (≠ `num_types`) toma el valor `counter[d]` de la secuencia del tipo `d`.

El prefijo de estado (versión + lista de tipos) se lee al inicio de cada bloque con filas > 0; los bloques de encabezado y los bloques vacíos no emiten nada.

> [!NOTE]
>
> **Recuentos malformados**
>
> `num_types` se lee del flujo antes que cualquier nombre de tipo. Un decodificador debe considerarlo no fiable y no debe usarlo directamente para dimensionar una asignación: ni un recuento cercano a `SIZE_MAX` (que puede desbordar la aritmética intermedia o lanzar una excepción distinta de `DB::Exception`) ni un recuento grande pero representable como `100000000` (que está muy por debajo del `max_size()` de un vector y, sin embargo, asignaría gigabytes antes de leer un solo nombre de tipo). ClickHouse lee la lista de tipos entrada a entrada, con solo una sugerencia de preasignación limitada, de modo que un `num_types` corrupto se rechaza como `INCORRECT_DATA` ("`Dynamic` column has too many types", cuando el recuento supera lo que el contenedor puede almacenar) o como un error de lectura ordinario en cuanto el flujo se queda sin entradas de tipo, pero nunca como un fallo por falta de memoria.
>
> Sin embargo, **no** limite el `num_types` **aplanado** con `ColumnDynamic::MAX_DYNAMIC_TYPES_LIMIT` (`254` en ClickHouse): la lista de tipos aplanada incluye *todos* los tipos de tiempo de ejecución distintos, incluidos los que se desbordaron hacia la variante compartida, por lo que un bloque aplanado válido puede enumerar legítimamente muchos más. El límite `MAX_DYNAMIC_TYPES_LIMIT` solo se aplica al recuento `num_dynamic_types` de los prefijos no aplanados `V1`/`V2`/`V3`, que cuenta las posiciones de variantes regulares y está acotado por dicho límite (ClickHouse lo valida ahí, antes de sumar el `+ 1` correspondiente a la variante compartida).

> [!NOTE]
>
> **Flujos truncados**
>
> La secuencia de discriminadores contiene exactamente `num_rows` entradas, y un decodificador debe comprobar que ha leído esa cantidad en lugar de aceptar lo que contenga el flujo. Una secuencia más corta no contradice nada de lo que viene después: los recuentos de valores por tipo se derivan de los discriminadores realmente leídos, por lo que siguen siendo coherentes con las secuencias de valores, y una secuencia compuesta íntegramente por marcadores NULL no requiere ningún byte de valor. ClickHouse rechaza una secuencia de discriminadores corta con `INCORRECT_DATA` ("Mismatch in flattened `Dynamic` column: N rows are expected, but the indexes stream contains only M rows") antes de cualquier comprobación por tipo, porque a continuación la columna reconstruida se usa para `num_rows` filas, independientemente de su tamaño real.

> [!NOTE]
>
> Los tipos de tiempo de ejecución con serialización con estado (`LowCardinality`, `Variant`, `Dynamic`, `JSON`) incluyen prefijos de estado anidados después de la lista de nombres de tipo.

La lista de tipos de tiempo de ejecución suele seguir la canonicalización de `Variant`: las posiciones de variantes regulares se escriben en el orden de `DataTypeVariant` (por nombre de tipo), de modo que el orden de transmisión no coincide con el orden de inserción. Sin embargo, **no siempre** está ordenada globalmente: los tipos que se desbordaron hacia la variante compartida (por ejemplo, con `Dynamic(max_types=N)`) se añaden tras las posiciones regulares en el orden en que aparecieron por primera vez, por lo que el final de la lista puede romper el orden por nombre de tipo. Por tanto, un decodificador debe tomar la lista de tipos transmitida como referencia autorizada para la asignación de discriminadores y no debe reordenarla por su cuenta. Lo que la lista no puede contener es un tipo repetido ni `Nothing`: dos entradas iguales corresponden a un único discriminador y `Nothing` ni siquiera es una variante, así que en ambos casos parte de la lista no tiene dónde reconstruirse. ClickHouse rechaza ambos casos con `INCORRECT_DATA`. Para las filas `[42::UInt64, "hi", NULL]`, los dos tipos son `String` y `UInt64`, y `"String"` va antes que `"UInt64"` en el orden, por lo que los discriminadores son `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
```

Reconstruido: fila 0 = UInt64 run\[0] = `42`; fila 1 = String run\[0] = `"hi"`; fila 2 = NULL. Las secuencias por tipo siguen el mismo orden de wire que la lista de tipos (`String` antes de `UInt64`).

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

La codificación JSON más completa: en lugar de aplanar cada valor como texto (Tier 1), la columna se divide en una subcolumna por cada ruta JSON. Se selecciona **sin** solicitar la alternativa de Tier 1 (`output_format_native_write_json_as_string = 0`) y con el indicador de serialización aplanada activado (`output_format_native_use_flattened_dynamic_and_json_serialization = 1`); en ese caso, el servidor emite la serialización **versión 3**.

Existen dos tipos de ruta:

* Las **rutas tipadas** se declaran en la cadena de tipo, por ejemplo `JSON(a UInt32, b String)`, y se decodifican con su tipo declarado. Si un nombre de ruta contiene puntos, se delimita con acentos graves en la cadena de tipo.
* Las **rutas dinámicas** se descubren en tiempo de ejecución y cada una se decodifica como una columna [Dynamic](#dynamic).

En el modo FLATTENED **no hay columna de datos compartidos** (ese almacén de desbordamiento es propio de las codificaciones de Object no aplanadas V2/V3). Cada ruta es una columna completa con `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 la estructura en dos fases: primero van **todos** los prefijos de estado de las rutas y después **todos** los datos de las rutas. Por lo tanto, el prefijo `Dynamic` de una ruta dinámica (en la fase de prefijos) queda separado de sus datos (en la fase de datos). El prefijo de estado se lee al inicio de cada bloque con filas > 0, y cada columna de ruta (tipada o dinámica) contiene exactamente `num_rows` valores. El objeto de la fila `r` se construye leyendo el valor de cada ruta en el índice `r`; una ruta dinámica cuyo discriminador `Dynamic` sea NULL en esa fila no aporta ninguna clave. Los nombres de las rutas dinámicas deben ser distintos y ninguno puede repetir una ruta que la cadena de tipo ya declare como tipada: en cualquiera de los dos casos, el objeto resultante tendría dos valores bajo una misma clave, por lo que ClickHouse rechaza ambos con `INCORRECT_DATA`. Lo mismo se aplica a la lista de rutas dinámicas de las codificaciones no aplanadas.

> [!NOTE]
>
> **Recuentos malformados**
>
> `num_dynamic_paths` en la disposición FLATTENED que se documenta aquí —y el recuento de rutas dinámicas en las codificaciones no aplanadas `V1`/`V2`— se lee del flujo antes que los nombres de las rutas. (Los prefijos no aplanados no incluyen un campo aparte para las rutas aplanadas: solo contienen el recuento de rutas dinámicas, además de un valor `max_dynamic_paths` en `V1` que se lee y se descarta). Al igual que con [`Dynamic`](#dynamic), un decodificador debe considerar estos recuentos como no fiables y no debe dimensionar una asignación directamente a partir de ellos, ya sea un recuento de la familia `SIZE_MAX` o uno grande pero representable. ClickHouse lee los nombres de las rutas de uno en uno, con solo una sugerencia de preasignación limitada, de modo que un recuento corrupto se rechaza como `INCORRECT_DATA` ("JSON/Object column has too many paths", cuando supera lo que el contenedor puede albergar) o como un error de lectura ordinario cuando el flujo se queda sin nombres de rutas.

> [!NOTE]
>
> **Flujos truncados**
>
> Los `num_rows` valores por columna de ruta son un requisito estricto, no un límite superior: un decodificador debe comprobar cada columna de ruta, tanto tipada como dinámica, con el recuento de filas del bloque, en lugar de confiar en que el flujo termine donde debería. El objeto resultante no tiene un tamaño propio con el que contrastar —su recuento de filas es el que haya producido el desaplanado—, por lo que una columna de ruta incompleta no puede detectarse más adelante, y leer su fila `r` implica leer más allá de su final. ClickHouse la rechaza con `INCORRECT_DATA` ("Unexpected size of typed path / flattened path *p* in flattened Object column"). En el caso de una ruta dinámica, esta comprobación se suma a la equivalente que se realiza dentro de su columna [`Dynamic`](#dynamic).

Valor `JSON` `{"a": 42, "b": "hi"}` (una fila, ambas rutas dinámicas). Un integer JSON se infiere 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 no aplanado (V1/V2)

Las codificaciones `Object` no aplanadas son las que el servidor emite on the wire cuando el indicador de aplanado está desactivado: `V1` a través de `clickhouse-client` / HTTP `FORMAT Native` (revisión `0`) y `V2` a través del protocolo TCP nativo. (`V3`, la tercera codificación no aplanada, solo se escribe en partes de MergeTree y se rechaza on the wire). Incluyen una columna shared-data y **no** se especifican en esta página. Tenga en cuenta que **no** incluyen estadísticas por ruta en el wire de Native: `NativeWriter` mantiene las estadísticas desactivadas, por lo que el prefijo de estructura de `Object` carece de sección de estadísticas y los bytes que lo siguen corresponden directamente a los prefijos typed/dynamic/shared-data y a los datos. Las estadísticas solo aparecen en las rutas en disco de MergeTree que las habilitan. Para decodificar una columna `JSON` con esta página, el Client debe elegir uno de los niveles documentados: establezca `output_format_native_write_json_as_string = 1` para el [String fallback](#json-tier-1-string-fallback), o bien `output_format_native_use_flattened_dynamic_and_json_serialization = 1` (con `output_format_native_write_json_as_string = 0`) para la disposición [FLATTENED Object](#json-tier-2-flattened-object).

## Trama de compresión

ClickHouse puede comprimir los datos de columna de un stream `Native` mediante un formato de trama interno. La [disposición de la trama](#frame-format) que se describe a continuación es **independiente del transporte** —las mismas tramas aparecen tanto en el protocolo TCP nativo como en HTTP—, pero la forma de solicitar la compresión, y lo que rodea a las tramas, varía según el transporte.

* **Protocolo TCP nativo.** La compresión es opcional y se activa por consulta mediante el indicador `compression` del [paquete Query](https://clickhouse.com/docs/es/resources/develop-contribute/native-protocol/protocol.md#query). Cuando está activa, el cuerpo de cada paquete `Data`, `Totals`, `Extremes`, `Log` y `ProfileEvents` —los bytes que siguen a la cadena `table_name`— se envuelve en el formato de trama. El sobre del paquete en sí, es decir, el código de tipo de paquete y la cadena `table_name`, **no** se comprime; el servidor los escribe directamente en el stream sin procesar. Todo lo que emite el `NativeWriter` va al stream comprimido, por lo que el prefijo `BlockInfo` es lo primero que aparece dentro de la trama, junto con las dimensiones y las columnas. Por tanto, un Client debe descomprimir la trama antes de poder leer `BlockInfo`.
* **HTTP.** `SELECT ... FORMAT Native&compress=1` envuelve todo el stream de bytes de `FORMAT Native` en las mismas tramas (el servidor usa el mismo `CompressedWriteBuffer` interno), y `?decompress=1` espera esas mismas tramas en un cuerpo de *entrada* `Native`, que decodifica mediante el `CompressedReadBuffer` correspondiente. En esta ruta no hay tipo de paquete TCP, `table_name` ni sobre de paquete: toda la carga útil comprimida se compone únicamente de bloques `Native` enmarcados (el prefijo `BlockInfo` solo está presente si la revisión negociada es mayor que `0`, igual que en la disposición sin comprimir descrita anteriormente). Este enmarcado interno de `compress`/`decompress` no debe confundirse con la compresión de transporte HTTP (`Content-Encoding: gzip`/`zstd`, habilitada mediante `enable_http_compression`), que envuelve la respuesta en la capa HTTP y no corresponde al formato de trama que se describe a continuación.

Así pues, un Client que solo haya implementado la disposición sin comprimir de `FORMAT Native` tendrá que añadir igualmente esta capa de tramas para leer una respuesta HTTP `Native` comprimida o para enviar un cuerpo de solicitud con `decompress=1`.

### Formato de trama

```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
```

El tamaño total de la trama es `16 + compressed_size` = `16 + 9 + body_size` = `25 + body_size`. Tenga en cuenta los dos intervalos: la suma de comprobación abarca el encabezado de 9 bytes y el cuerpo, mientras que `compressed_size` incluye el encabezado y el cuerpo, pero **no** la propia suma de comprobación:

![Campos de la trama de compresión y límite del tamaño 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 del byte de método

Estos tres códecs son los que el servidor genera para el enmarcado `Native` de stream completo: la salida HTTP con `compress=1` utiliza el códec predeterminado del servidor (`ZSTD(3)`), y el protocolo TCP nativo utiliza `LZ4`, `ZSTD` o `NONE` según `network_compression_method`. A un Client `Native` genérico le basta con generar y consumir estos tres.

| Byte | Método | Codificación del cuerpo |
| - | - | - |
| `0x02` | NONE | El cuerpo son los bytes sin procesar (sin compresión). La trama se emite igualmente; el receptor verifica la suma de comprobación. |
| `0x82` | LZ4 | El cuerpo está en el **formato de bloque LZ4**, *no* en el formato de trama LZ4. Sin número mágico. |
| `0x90` | ZSTD | El cuerpo es un stream zstd sin procesar de una sola trama (el número mágico estándar de zstd forma parte del cuerpo). |

El byte de método también codifica los [códecs a nivel de columna](https://clickhouse.com/docs/es/reference/statements/create/table/codec.md). Estos se aplican por columna en las rutas en disco de MergeTree y no al enmarcado de stream completo, pero la ruta de entrada HTTP con `decompress=1` toma el códec del byte de método de cada trama, por lo que cualquiera de estos bytes puede aparecer legítimamente en la entrada. Por tanto, un decodificador conforme debe reconocer todo el espacio asignado y rechazar los bytes que no implemente, en lugar de interpretar erróneamente el cuerpo. Sus cuerpos son específicos de cada códec y quedan fuera de este contrato genérico de trama:

| Byte | Método |
| - | - |
| `0x91` | `Multiple` (un códec compuesto que envuelve una secuencia de códecs anidados) |
| `0x92` | `Delta` |
| `0x93` | `T64` |
| `0x94` | `DoubleDelta` |
| `0x95` | `Gorilla` |
| `0x96` | `AES_128_GCM_SIV` (cifrado) |
| `0x97` | `AES_256_GCM_SIV` (cifrado) |
| `0x98` | `FPC` |
| `0x9a` | `GCD` |
| `0x9c` | `ALP` |
| `0x9d` | `SZ3` |
| `0x9e` | `Quantized` |
| `0x9f` | `ZXC` |

`DoubleDelta` (`0x94`) almacena las segundas diferencias como bits de prefijo/signo/magnitud, empezando por el MSB. La decodificación basada en palabras conserva esta codificación, el número de elementos y el relleno del último byte.

`0x9c` (`ALP`) es un códec sin pérdida en **beta** para `Float32` y `Float64` (compresión adaptativa sin pérdida de coma flotante). Solo se puede crear una tabla con `CODEC(ALP)` cuando el ajuste `enable_alp_codec` está activado, pero el byte de método siempre se acepta al descomprimir para que los datos escritos previamente sigan siendo legibles. `0x9d` (`SZ3`) es un códec *con pérdida* **experimental** y con error acotado para `Float32`, `Float64` y `Array` de esos tipos. Solo se puede crear una tabla con `CODEC(SZ3)` cuando el ajuste `enable_sz3_codec` está activado, pero el byte de método siempre se acepta al descomprimir para que los datos escritos previamente sigan siendo legibles. `0x9f` (`ZXC`) es un códec LZ asimétrico **experimental**: lento al comprimir, pero muy rápido al descomprimir, con una tasa de compresión intermedia entre `LZ4` y `ZSTD`. Solo se puede crear una tabla con `CODEC(ZXC)` cuando el ajuste `enable_zxc_codec` está activado, pero el byte de método siempre se acepta al descomprimir para que los datos escritos previamente sigan siendo legibles. Los bytes `0x99` (`DeflateQpl`) y `0x9b` (`ZSTD_QPL`) se asignaron a códecs que ya se han eliminado; están reservados y no se reutilizan.

`0x9e` (`Quantized`) es un códec de columna **experimental** para columnas de vectores densos (`Array(Float32)` y similares). Al igual que `NONE`, es de paso directo (el cuerpo de precisión completa se almacena tal cual), pero su presencia asocia una serialización que escribe un stream complementario, compacto y cuantizado, que se utiliza para acelerar la búsqueda vectorial. Solo se puede crear una tabla con `CODEC(Quantized(...))` cuando el ajuste `enable_quantized_codec` está activado, y el byte de método siempre se acepta al descomprimir.

### Suma de comprobación

ClickHouse utiliza CityHash v1.0.2 (la variante histórica), **no** el CityHash moderno de Google; ambos generan resultados distintos.

La suma de comprobación se calcula sobre los 9 bytes del encabezado (method + compressed\_size + uncompressed\_size) más los N bytes del cuerpo, es decir, todo lo que hay entre la suma de comprobación y el final de la trama. Los primeros 8 bytes de la salida de 16 bytes de CityHash128 corresponden a la mitad baja (LE) y los 8 bytes siguientes, a la mitad alta (LE). El decodificador vuelve a calcular el CityHash128 sobre el encabezado y el cuerpo recibidos y lo compara con los 16 bytes iniciales; si no coinciden, los datos se consideran corruptos y el decodificador falla.

### Límites por bloque

La carga útil comprimida de un bloque es un **flujo de una o más tramas**, no necesariamente una sola trama. El emisor escribe el bloque serializado a través de un `CompressedWriteBuffer` que emite una trama cada vez que su búfer interno se llena (≈1 MB, `DBMS_DEFAULT_BUFFER_SIZE`) y una trama final cuando se vacía el bloque. Así, un bloque pequeño ocupa una sola trama, mientras que un bloque grande ocupa varias tramas consecutivas.

El invariante funciona en un solo sentido: dado que el emisor vacía el búfer comprimido al final de cada bloque, **cada final de bloque coincide con un límite de trama**, pero lo contrario no se cumple. Un límite de trama intermedio, emitido porque el búfer se llenó a mitad de un bloque, queda en *medio* del bloque y no constituye un límite de bloque. Por lo tanto, un decodificador debe usar las propias dimensiones del bloque (`num_columns`/`num_rows`) para determinar dónde termina; no debe suponer que cada trama corresponde a un bloque completo.

Un receptor procesa las tramas en flujo: lee 16 + 9 bytes, lee exactamente `compressed_size - 9` bytes del cuerpo, los descomprime en exactamente `uncompressed_size` bytes y entrega esos bytes al decodificador de bloques; cuando el decodificador necesita más datos de los que contiene la trama actual, obtiene la siguiente. Como el emisor vacía el búfer en cada bloque, una vez que un bloque se ha decodificado por completo, el búfer de tramas queda vacío y el siguiente bloque comienza en una trama nueva.

En el protocolo TCP nativo, el sobre del paquete (el VarUInt del tipo de paquete y la cadena `table_name`) se escribe en el flujo **sin procesar**, fuera de la carga útil comprimida; solo el cuerpo del bloque (BlockInfo + columnas) se encapsula en tramas. La ruta HTTP `compress`/`decompress` carece de dicho sobre: todo el flujo está formado por bloques encapsulados en tramas.

### Negociación

En el protocolo TCP nativo, la compresión se aplica por consulta, no por conexión. El campo `compression: bool` del paquete Query la solicita solo para esa consulta. El servidor atiende la solicitud y emite cuerpos `Data`/`Totals`/`Extremes`/`Log`/`ProfileEvents` comprimidos durante toda la duración de la consulta (`Log`/`ProfileEvents` solo a partir de v54481). Asimismo, espera que los bloques Data *salientes* del Client (tablas externas, el marcador vacío de fin de datos y las filas de INSERT) se enmarquen de la misma forma. Las consultas posteriores en la misma conexión pueden comportarse de otra manera.

En HTTP no existe el paquete Query: el parámetro de consulta `compress=1` selecciona la salida enmarcada para esa solicitud, y `decompress=1` indica que el cuerpo de la solicitud está enmarcado. La salida de `compress=1` se escribe con el códec predeterminado del servidor (`ZSTD(3)`) y no con `network_compression_method`; el lector de `decompress=1` obtiene el códec del byte de método de cada trama, por lo que en la entrada se acepta cualquier códec.

> [!NOTE]
>
> Con la compresión activada, el servidor también puede encaminar las columnas por la ruta de serialización paralela de bloques / `ColumnBLOB` (`PARALLEL_BLOCK_MARSHALLING`, v54478) en bloques con más de una fila. Una implementación que comprima datos de INSERT debe estar preparada para gestionar esa ruta (o desactivarla explícitamente) a fin de evitar que el stream se desincronice.

## Glosario

**Bloque** — la unidad de intercambio de datos en el formato Native. Un fragmento autodescriptivo de filas almacenadas en formato columnar. Consulte [estructura de bloques y columnas](#block-and-column-structure).

**BlockInfo** — el encabezado de metadata que precede a un bloque en la ruta de paquetes Data de TCP (se escribe siempre que la revisión de la conexión sea mayor que cero). Consiste en una secuencia de campos etiquetados con un ID de campo y condicionados por la revisión. El formato de salida `Native` lo omite, ya que serializa con la revisión `0`. Consulte [BlockInfo](#blockinfo).

**Cuerpo de columna** — los bytes de una columna que contienen los valores reales y que siguen al encabezado de la columna (nombre, tipo, byte has\_custom\_serialization). La disposición depende del tipo. Consulte [disposición de columnas en el wire](#column-wire-layout).

**Tipo compuesto** — un tipo formado por uno o más tipos internos, que se codifica como varios streams por columna. El formato wire es estable y no está versionado. Consulte [tipos compuestos](#composite-types).

**Diccionario (LowCardinality)** — el array de valores únicos al que una columna `LowCardinality(T)` hace referencia mediante índices enteros. Consulte [LowCardinality](#lowcardinality).

**Bloque vacío** — un bloque con `num_columns = 0` y `num_rows = 0`. Se utiliza como centinela: como marcador de fin de entrada en el lado del Client y como marcador de límite de stream en el lado del servidor. Consulte [variantes de bloque](#block-variants).

**Bloque de encabezado** — un bloque con `num_columns > 0` y `num_rows = 0` que el servidor envía como primer paquete Data de la respuesta a una consulta. Anuncia el esquema del resultado. Consulte [variantes de bloque](#block-variants).

**Tipo interno** — el tipo que envuelve un tipo compuesto. El tipo interno de `Array(UInt32)` es `UInt32`; el de `Nullable(T)` es `T`.

**Stream de desplazamientos** — el array UInt64 de posiciones finales acumuladas que `Array`, `Map` y `Nested` utilizan para delimitar los elementos de cada fila. Consulte [Array](#array).

**Valor de relleno** — los bytes que se escriben en las posiciones nulas del stream de valores de una columna `Nullable(T)`. El decodificador los lee para avanzar en el stream, pero ignora su contenido. Consulte [Nullable](#nullable).

**Bloque de resultado** — un bloque con `num_rows > 0` que contiene filas reales del resultado de una consulta. Consulte [variantes de bloque](#block-variants).

**Bloque de esquema** — sinónimo de bloque de encabezado que se emplea al describir la fase de INSERT, en la que el bloque de esquema indica al Client la forma esperada de las columnas.

**Versión de serialización** — un número de versión en el wire, específico de cada tipo, con el que los tipos versionados declaran qué variante de la codificación viene a continuación. No debe confundirse con la versión del protocolo. Consulte [versión de serialización: concepto](#serialization-version-concept).

**Prefijo de estado** — los bytes que preceden a la carga útil de cada bloque en un tipo versionado. Contiene la versión de serialización y (en el caso de LowCardinality) la metadata del diccionario de cada bloque. Se emite al inicio de cada bloque con filas > 0 y no se conserva de un bloque a otro.

**Stream** — una secuencia contigua de bytes dentro del cuerpo de una columna que codifica un subcomponente lógico (un mapa de nulos, un array de desplazamientos, un stream de valores). Los tipos multistream concatenan dos o más streams por columna.
