Saltar al contenido principal
EntradaSalidaAlias

Descripción

El formato Native es el formato más eficiente de ClickHouse porque es realmente “columnar”, ya que no convierte las columnas en filas. En este formato, los datos se escriben y se leen por bloques en formato binario. Para cada bloque, se registran uno tras otro el número de filas, el número de columnas, los nombres y tipos de las columnas, y las partes de las columnas del bloque. Este es el formato que se utiliza en la interfaz nativa para la interacción entre servidores, para usar el cliente de línea de comandos y para los clientes de C++.
Puede usar este formato para generar rápidamente volcados que solo puede leer el sistema de gestión de bases de datos ClickHouse. Puede que no resulte práctico trabajar directamente con este formato.

Formato wire de los tipos de datos

Los datos se envían por el wire en un formato columnar, lo que significa que cada columna se envía por separado y que todos los valores de una columna se envían juntos como un único array. Cada columna de un bloque contiene un encabezado similar a RowBinaryWithNamesAndTypes.
Al usar el protocolo binario TCP nativo (o cuando el endpoint HTTP recibe ?client_protocol_version=<n>), se escribe una estructura BlockInfo antes del recuento de columnas y filas. Los ejemplos de esta sección usan la interfaz HTTP simple, sin versión de protocolo, por lo que se omite BlockInfo.

Estructura del bloque

La siguiente consulta devuelve dos columnas, number y str, con tres filas:
curl -XPOST "http://localhost:8123?default_format=Native" --data-binary "SELECT number, toString(number) AS str FROM system.numbers LIMIT 3" > out.bin
Los datos de salida caben en un solo bloque de ClickHouse y tendrán este aspecto:
const data = new Uint8Array([
  // --- Encabezado de bloque ---
  0x02,                   // 2 columnas
  0x03,                   // 3 filas
  // -- Encabezado de columna 1 --
  0x06,                   // LEB128 - el nombre de columna 'number' tiene 6 bytes
  0x6e, 0x75, 0x6d,       
  0x62, 0x65, 0x72,       // nombre de columna: 'number'
  0x06,                   // LEB128 - el tipo de columna 'UInt64' tiene 6 bytes
  0x55, 0x49, 0x6e,
  0x74, 0x36, 0x34,       // 'UInt64'
  0x00, 0x00, 0x00, 0x00, 
  0x00, 0x00, 0x00, 0x00, // 0 como UInt64
  0x01, 0x00, 0x00, 0x00, 
  0x00, 0x00, 0x00, 0x00, // 1 como UInt64
  0x02, 0x00, 0x00, 0x00, 
  0x00, 0x00, 0x00, 0x00, // 2 como UInt64
  0x03,                   // LEB128 - el nombre de columna 'str' tiene 3 bytes
  0x73, 0x74, 0x72,       // nombre de columna: 'str'
  0x06,                   // LEB128 - el tipo de columna 'String' tiene 6 bytes
  0x53, 0x74, 0x72, 
  0x69, 0x6e, 0x67,       // 'String'
  0x01,                   // LEB128 - la cadena tiene 1 byte
  0x30,                   // '0' como String
  0x01,                   // LEB128 - la cadena tiene 1 byte
  0x31,                   // '1' como String
  0x01,                   // LEB128 - la cadena tiene 1 byte
  0x32,                   // '2' como String
])

Múltiples bloques

Sin embargo, en muchos casos, los datos no cabrán en un solo bloque, y ClickHouse los enviará en varios bloques. Considere la siguiente consulta, que recupera dos filas con un tamaño de bloque reducido para forzar que los datos se dividan en una fila por bloque:
curl -XPOST "http://localhost:8123?default_format=Native" --data-binary "SELECT number, toString(number) AS str                FROM system.numbers LIMIT 2                 SETTINGS max_block_size=1" \  > out.bin
La salida:
const data = new Uint8Array([
 
  // ----- Bloque 1 ----- 
  0x02,                   // 2 columnas
  0x01,                   // 1 fila
  0x06,                   // LEB128 - el nombre de columna 'number' tiene 6 bytes
  0x6E, 0x75, 0x6D, 
  0x62, 0x65, 0x72,       // nombre de columna: 'number' 
  0x06,                   // LEB128 - el tipo de columna 'UInt64' tiene 6 bytes
  0x55, 0x49, 0x6E, 
  0x74, 0x36, 0x34,       // 'UInt64' 
  0x00, 0x00, 0x00, 0x00, 
  0x00, 0x00, 0x00, 0x00, // 0 como UInt64
  0x03,                   // LEB128 - el nombre de columna 'str' tiene 3 bytes
  0x73, 0x74, 0x72,       // nombre de columna: 'str'
  0x06,                   // LEB128 - el tipo de columna 'String' tiene 6 bytes
  0x53, 0x74, 0x72, 
  0x69, 0x6E, 0x67,       // 'String'
  0x01,                   // LEB128 - la cadena tiene 1 byte
  0x30,                   // '0' como String
  
  // ----- Bloque 2 -----
  0x02,                   // 2 columnas
  0x01,                   // 1 fila
  0x06,                   // LEB128 - el nombre de columna 'number' tiene 6 bytes
  0x6E, 0x75, 0x6D,  
  0x62, 0x65, 0x72,       // nombre de columna: 'number'
  0x06,                   // LEB128 - el tipo de columna 'UInt64' tiene 6 bytes
  0x55, 0x49, 0x6E,  
  0x74, 0x36, 0x34,       // 'UInt64'
  0x01, 0x00, 0x00, 0x00,  
  0x00, 0x00, 0x00, 0x00, // 1 como UInt64
  0x03,                   // LEB128 - el nombre de columna 'str' tiene 3 bytes
  0x73, 0x74, 0x72,       // nombre de columna: 'str'
  0x06,                   // LEB128 - el tipo de columna 'String' tiene 6 bytes
  0x53, 0x74, 0x72,  
  0x69, 0x6E, 0x67,       // 'String'
  0x01,                   // LEB128 - la cadena tiene 1 byte
  0x31,                   // '1' como String
]);

Tipos de datos simples

El formato wire de un valor individual de uno de los tipos de datos simples es similar a RowBinary/RowBinaryWithNamesAndTypes. La lista completa de tipos que se ajustan a esta descripción incluye:
  • (U)Int8, (U)Int16, (U)Int32, (U)Int64, (U)Int128, (U)Int256
  • Float32, Float64
  • Bool
  • String
  • FixedString(N)
  • Date
  • Date32
  • DateTime
  • DateTime64
  • IPv4
  • IPv6
  • UUID
Consulta las descripciones de los tipos anteriores en “Formato wire de los tipos de datos de RowBinary” para obtener más información.

Tipos de datos complejos

La codificación de los siguientes tipos de datos difiere de RowBinary y RowBinaryWithNamesAndTypes.
  • Nullable
  • LowCardinality
  • Array
  • Map
  • Variant
  • Dynamic
  • JSON

Nullable

En el formato Native, una columna Nullable tendrá un número de bytes igual al número de filas del bloque antes de los datos propiamente dichos. Cada uno de estos bytes indica si el valor es NULL o no. Por ejemplo, con esta consulta, cada número impar será NULL:
curl -XPOST "http://localhost:8123?default_format=Native" \  --data-binary "SELECT if(number % 2 = 0, number, NULL) :: Nullable(UInt64) AS maybe_null                 FROM system.numbers LIMIT 5" \  > out.bin
La salida tendrá este aspecto:
const data = new Uint8Array([
  // --- Encabezado de bloque ---
  0x01,                         // LEB128 - 1 columna
  0x05,                         // LEB128 - 5 filas
  
  // -- Encabezado de columna --
  0x0A,                         // LEB128 - el nombre de columna tiene 10 bytes
  0x6D, 0x61, 0x79, 0x62, 0x65, 
  0x5F, 0x6E, 0x75, 0x6C, 0x6C, // nombre de columna: 'maybe_null'
  
  0x10,                         // LEB128 - el tipo de columna tiene 16 bytes
  0x4E, 0x75, 0x6C, 0x6C, 
  0x61, 0x62, 0x6C, 0x65, 
  0x28, 0x55, 0x49, 0x6E, 
  0x74, 0x36, 0x34, 0x29,       // tipo de columna: 'Nullable(UInt64)'
  
  // -- Máscara Nullable --
  0x00,                         // La fila 0 es NOT NULL
  0x01,                         // La fila 1 es NULL
  0x00,                         // La fila 2 es NOT NULL
  0x01,                         // La fila 3 es NULL
  0x00,                         // La fila 4 es NOT NULL
  
  // -- Valores UInt64 --
  0x00, 0x00, 0x00, 0x00, 
  0x00, 0x00, 0x00, 0x00,       // Fila 0: 0 como UInt64

  // aunque pueda haber un valor válido para este número
  // en el bloque, igualmente debe devolverse como NULL al usuario.
  0x01, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00,       // Fila #1: NULL
  
  0x02, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00,       // Fila #2: 2 como UInt64
  
  0x03, 0x00, 0x00, 0x00, 
  0x00, 0x00, 0x00, 0x00,       // Fila #3: NULL, similar a la fila #1
  
  0x04, 0x00, 0x00, 0x00, 
  0x00, 0x00, 0x00, 0x00,       // Fila #4: 4 como UInt64
]);
Funciona de forma similar con Nullable(String). El indicador de nulo siempre proviene del byte de máscara de Nullable: un valor de máscara de 0x01 significa que la fila es NULL independientemente del contenido de la cadena. En las filas NULL, la cadena subyacente se almacena como una cadena vacía (longitud LEB128 0). Tenga en cuenta que una cadena vacía no NULL también tiene una longitud LEB128 de 0, por lo que solo el byte de máscara distingue ambos casos. Por ejemplo, la siguiente consulta:
curl -XPOST "http://localhost:8123?default_format=Native" \  --data-binary "SELECT if(number % 2 = 0, toString(number), NULL) :: Nullable(String) AS maybe_str                 FROM system.numbers LIMIT 5" \  > out.bin
La salida tendrá este aspecto:
const data = new Uint8Array([
  // --- Encabezado de bloque ---
  0x01, // LEB128 - 1 columna
  0x05, // LEB128 - 5 filas

  // -- Encabezado de columna --
  0x09, // LEB128 - el nombre de columna tiene 9 bytes
  0x6d,
  0x61,
  0x79,
  0x62,
  0x65,
  0x5f,
  0x73,
  0x74,
  0x72, // nombre de columna: 'maybe_str'

  0x10, // LEB128 - el tipo de columna tiene 16 bytes
  0x4e,
  0x75,
  0x6c,
  0x6c,
  0x61,
  0x62,
  0x6c,
  0x65,
  0x28,
  0x53,
  0x74,
  0x72,
  0x69,
  0x6e,
  0x67,
  0x29, // tipo de columna: 'Nullable(String)'

  // -- Máscara Nullable --
  0x00, // Fila 0 es NOT NULL
  0x01, // Fila 1 es NULL
  0x00, // Fila 2 es NOT NULL
  0x01, // Fila 3 es NULL
  0x00, // Fila 4 es NOT NULL

  // -- Valores String --
  0x01,
  0x30, // Fila 0: LEB128 == 1, '0' como String
  0x00, // Fila 1: LEB128 == 0, NULL
  0x01,
  0x32, // Fila 2: LEB128 == 1, '2' como String
  0x00, // Fila 3: LEB128 == 0, NULL
  0x01,
  0x34, // Fila 4: LEB128 == 1, '4' como String
])

LowCardinality

A diferencia de RowBinary, donde LowCardinality es transparente, el formato Native utiliza una codificación columnar basada en diccionario. Una columna se codifica como un prefijo de versión, seguido de un diccionario de valores únicos y un array de índices enteros dentro de ese diccionario.
Una columna puede definirse como LowCardinality(Nullable(T)), pero no es posible definirla como Nullable(LowCardinality(T)) — siempre dará como resultado un error del servidor.
El prefijo de versión es un UInt64(LE) con valor 1, escrito una vez por columna. Luego, por bloque, se escribe lo siguiente:
  • UInt64(LE) — campo de bits IndexesSerializationType. Los bits 0–7 codifican el ancho del índice (0 = UInt8, 1 = UInt16, 2 = UInt32, 3 = UInt64). El bit 8 (NeedGlobalDictionaryBit) nunca se establece en el formato Native (el servidor genera una excepción si lo encuentra). El bit 9 indica que hay claves de diccionario adicionales presentes. El bit 10 indica que el diccionario debe restablecerse.
  • UInt64(LE) — número de claves del diccionario, seguido de las claves serializadas en bloque mediante la codificación del tipo interno.
  • UInt64(LE) — número de filas, seguido de los valores de índice serializados en bloque usando el ancho de UInt correspondiente.
El diccionario siempre contiene un valor predeterminado en el índice 0 (por ejemplo, una cadena vacía para String, 0 para los tipos numéricos). Para LowCardinality(Nullable(T)), el índice 0 representa NULL, y las claves se serializan sin el contenedor Nullable. Por ejemplo, LowCardinality(String) con 5 filas ['foo', 'bar', 'baz', 'foo', 'bar']:
// Prefijo de versión
01 00 00 00 00 00 00 00    // UInt64(LE) = 1

// IndexesSerializationType: índices UInt8, tiene claves, actualizar diccionario
00 06 00 00 00 00 00 00    // UInt64(LE) = 0x0600

04 00 00 00 00 00 00 00    // 4 claves del diccionario
00                          // clave 0: "" (predeterminado)
03 66 6f 6f                 // clave 1: "foo"
03 62 61 72                 // clave 2: "bar"
03 62 61 7a                 // clave 3: "baz"

05 00 00 00 00 00 00 00    // 5 filas
01 02 03 01 02              // índices → "foo", "bar", "baz", "foo", "bar"
Con LowCardinality(Nullable(String)), el índice 0 es NULL:
01 00 00 00 00 00 00 00    // versión
00 06 00 00 00 00 00 00    // IndexesSerializationType
03 00 00 00 00 00 00 00    // 3 claves
00                          // clave 0: NULL
00                          // clave 1: "" (predeterminado)
03 79 65 73                 // clave 2: "yes"
05 00 00 00 00 00 00 00    // 5 filas
02 00 02 00 02              // índices → "yes", NULL, "yes", NULL, "yes"

Array

A diferencia de RowBinary, donde cada array va precedido de un recuento LEB128 del número de elementos, el formato Native codifica los arrays como dos subflujos columnares:
  • N offsets UInt64 acumulativos (little-endian, 8 bytes cada uno). La fila i tiene offset[i] - offset[i-1] elementos, con offset[-1] implícitamente igual a 0.
  • Todos los elementos anidados de todas las filas, serializados en bloque de forma contigua.
Por ejemplo, Array(UInt32) con 3 filas [[0, 10], [1, 11], [2, 12]]:
// Desplazamientos
02 00 00 00 00 00 00 00    // 2 (fila 0: 2 elementos)
04 00 00 00 00 00 00 00    // 4 (fila 1: 2 elementos)
06 00 00 00 00 00 00 00    // 6 (fila 2: 2 elementos)

// Valores UInt32 anidados (6 en total)
00 00 00 00                 // 0
0a 00 00 00                 // 10
01 00 00 00                 // 1
0b 00 00 00                 // 11
02 00 00 00                 // 2
0c 00 00 00                 // 12
Un array vacío tiene el mismo desplazamiento que la fila anterior. Por ejemplo, Array(String) con 4 filas [[], ['0'], ['0','1'], ['0','1','2']]:
00 00 00 00 00 00 00 00    // 0 (vacío)
01 00 00 00 00 00 00 00    // 1
03 00 00 00 00 00 00 00    // 3
06 00 00 00 00 00 00 00    // 6
01 30                       // "0"
01 30                       // "0"
01 31                       // "1"
01 30                       // "0"
01 31                       // "1"
01 32                       // "2"

Map

Un Map(K, V) se codifica como Array(Tuple(K, V)) — offsets del array, seguidos de todas las claves y luego de todos los valores. Esto difiere de RowBinary, donde las claves y los valores se intercalan en cada entrada. Por ejemplo, Map(String, UInt64) con 3 filas [{'a':0,'b':10}, {'a':1,'b':11}, {'a':2,'b':12}]:
// Desplazamientos del array
02 00 00 00 00 00 00 00    // 2
04 00 00 00 00 00 00 00    // 4
06 00 00 00 00 00 00 00    // 6

// Todas las claves (6 Strings)
01 61                       // "a"
01 62                       // "b"
01 61                       // "a"
01 62                       // "b"
01 61                       // "a"
01 62                       // "b"

// Todos los valores (6 UInt64s)
00 00 00 00 00 00 00 00    // 0
0a 00 00 00 00 00 00 00    // 10
01 00 00 00 00 00 00 00    // 1
0b 00 00 00 00 00 00 00    // 11
02 00 00 00 00 00 00 00    // 2
0c 00 00 00 00 00 00 00    // 12

Variant

A diferencia de RowBinary, donde cada fila lleva su propio byte discriminante seguido del valor en línea, el formato Native separa los discriminantes de los datos.
Al igual que en RowBinary, los tipos de la definición siempre se ordenan alfabéticamente, y el discriminante es el índice dentro de esa lista ordenada. 0xFF (255) representa NULL.
Una columna Variant se codifica de la siguiente manera:
  • Prefijo del modo de discriminadores UInt64(LE) (0 = BASIC, 1 = COMPACT). La salida del formato Native suele usar BASIC (0); el modo COMPACT puede aparecer al leer datos almacenados con use_compact_variant_discriminators_serialization activado.
  • N discriminadores UInt8, uno por fila.
  • Los datos de cada tipo de variante como una columna en bloque independiente que contiene solo las filas correspondientes, en orden de discriminante.
Por ejemplo, Variant(String, UInt32) con 5 filas [0::UInt32, 'hello', NULL, 3::UInt32, 'hello'] (ordenados: String = 0, UInt32 = 1):
00 00 00 00 00 00 00 00    // modo de discriminadores = BASIC
01 00 ff 01 00              // UInt32, String, NULL, UInt32, String

// String (2 valores, filas 1 y 4)
05 68 65 6c 6c 6f          // "hello"
05 68 65 6c 6c 6f          // "hello"

// UInt32 (2 valores, filas 0 y 3)
00 00 00 00                 // 0
03 00 00 00                 // 3

Dynamic

A diferencia de RowBinary, donde cada valor es autodescriptivo (prefijo de tipo + valor), el formato Native serializa Dynamic como un prefijo de estructura seguido de una columna Variant. El prefijo de estructura contiene una versión de serialización UInt64(LE), luego el número de tipos dinámicos (como VarUInt) y, a continuación, los nombres de los tipos como cadenas. En la versión V1, el recuento de tipos se escribe dos veces por compatibilidad. Los datos que siguen corresponden a una columna Variant cuya lista de tipos incluye los tipos dinámicos más un tipo interno SharedVariant, ordenados alfabéticamente. Por ejemplo, Dynamic con 5 filas [0::UInt32, 'hello', NULL, 3::UInt32, 'hello']:
// Prefijo de estructura (V1)
01 00 00 00 00 00 00 00    // versión = V1
02                          // núm. de tipos (V1 escribe dos veces)
02                          // núm. de tipos
06 53 74 72 69 6e 67       // "String"
06 55 49 6e 74 33 32       // "UInt32"

// Datos Variant: Variant(SharedVariant, String, UInt32)
// discriminantes: SharedVariant=0, String=1, UInt32=2
00 00 00 00 00 00 00 00    // modo de discriminadores = BASIC
02 01 ff 02 01              // UInt32, String, NULL, UInt32, String
// SharedVariant: 0 valores
05 68 65 6c 6c 6f          // String: "hello"
05 68 65 6c 6c 6f          // String: "hello"
00 00 00 00                 // UInt32: 0
03 00 00 00                 // UInt32: 3

JSON

A diferencia de RowBinary, donde cada fila contiene su propia descripción con nombres de rutas y valores, el formato Native serializa JSON en una estructura columnar. La codificación es compleja y depende de la versión: consta de un prefijo de estructura con la versión de serialización, nombres de rutas dinámicas y la disposición de los datos compartidos, seguido de rutas tipadas (cada una como una columna en bloque), rutas dinámicas (cada una como una columna Dynamic) y datos compartidos para las rutas de desbordamiento. Para una interoperabilidad más sencilla, considere usar la configuración output_format_native_write_json_as_string=1, que serializa las columnas JSON como cadenas de texto JSON simples (una String por fila).
Última modificación el 10 de junio de 2026