| Entrada | Salida | Alias |
|---|---|---|
| ✔ | ✔ |
Descripción
La especificación oficial completa del formato Native está disponible aquí, y la especificación complementaria del protocolo Native —el protocolo TCP wire que lo transporta— está disponible aquí.
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++.
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.
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.binLos 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.binLa 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.binLa 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.binLa 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.
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 bitsIndexesSerializationType. 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
UInt64acumulativos (little-endian, 8 bytes cada uno). La filaitieneoffset[i] - offset[i-1]elementos, conoffset[-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 // 12Un 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 // 12Variant
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.
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 conuse_compact_variant_discriminators_serializationactivado. - 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 // 3Dynamic
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: 3JSON
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).