Native 格式是 ClickHouse 中最高效的格式,因为它是真正意义上的“列式”格式,
不会将列转换为行。
在这种格式下,数据以二进制格式按块进行读写。
对于每个块,会依次记录行数、列数、列名和类型,以及该块中列的数据部分。
这是在原生接口中用于服务器之间交互、使用命令行客户端以及 C++ 客户端时所使用的格式。
提示
你可以使用这种格式快速生成只能由 ClickHouse 数据库管理系统 (DBMS) 读取的转储文件。
自己直接使用这种格式进行操作可能并不实用。
数据在传输时采用列式格式,这意味着每一列都会单独发送,
并且某一列中的所有值会作为一个数组一起发送。
块中的每一列都包含与 RowBinaryWithNamesAndTypes 类似的头部信息。
注意
使用原生 TCP 二进制协议时 (或者当 HTTP 端点接收 ?client_protocol_version=<n> 时) ,
会在列数和行数之前写入一个 BlockInfo 结构。本节中的示例使用的是
不带协议版本的普通 HTTP 接口,因此会省略 BlockInfo。
块结构
以下查询返回两列:number 和 str,共三行:
curl -XPOST "http://localhost:8123?default_format=Native" --data-binary "SELECT number, toString(number) AS str FROM system.numbers LIMIT 3" > out.bin
输出数据会落在单个 ClickHouse 数据块中,形式如下:
const data = new Uint8Array([
// --- Block Header ---
0x02, // 2 columns
0x03, // 3 rows
// -- Column 1 Header --
0x06, // LEB128 - column name 'number' has 6 bytes
0x6e, 0x75, 0x6d,
0x62, 0x65, 0x72, // column name: 'number'
0x06, // LEB128 - column type 'UInt64' has 6 bytes
0x55, 0x49, 0x6e,
0x74, 0x36, 0x34, // 'UInt64'
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, // 0 as UInt64
0x01, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, // 1 as UInt64
0x02, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, // 2 as UInt64
0x03, // LEB128 - column name 'str' has 3 bytes
0x73, 0x74, 0x72, // column name: 'str'
0x06, // LEB128 - column type 'String' has 6 bytes
0x53, 0x74, 0x72,
0x69, 0x6e, 0x67, // 'String'
0x01, // LEB128 - the string has 1 byte
0x30, // '0' as String
0x01, // LEB128 - the string has 1 byte
0x31, // '1' as String
0x01, // LEB128 - the string has 1 byte
0x32, // '2' as String
])
多个块
不过,在很多情况下,数据无法装入单个块,ClickHouse 会将数据分成多个块发送。
请看下面这个查询:它获取两行数据,并通过减小块大小,强制将数据拆分为每个块一行:
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
输出:
const data = new Uint8Array([
// ----- Block 1 -----
0x02, // 2 columns
0x01, // 1 row
0x06, // LEB128 - column name 'number' has 6 bytes
0x6E, 0x75, 0x6D,
0x62, 0x65, 0x72, // column name: 'number'
0x06, // LEB128 - column type 'UInt64' has 6 bytes
0x55, 0x49, 0x6E,
0x74, 0x36, 0x34, // 'UInt64'
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, // 0 as UInt64
0x03, // LEB128 - column name 'str' has 3 bytes
0x73, 0x74, 0x72, // column name: 'str'
0x06, // LEB128 - column type 'String' has 6 bytes
0x53, 0x74, 0x72,
0x69, 0x6E, 0x67, // 'String'
0x01, // LEB128 - the string has 1 byte
0x30, // '0' as String
// ----- Block 2 -----
0x02, // 2 columns
0x01, // 1 row
0x06, // LEB128 - column name 'number' has 6 bytes
0x6E, 0x75, 0x6D,
0x62, 0x65, 0x72, // column name: 'number'
0x06, // LEB128 - column type 'UInt64' has 6 bytes
0x55, 0x49, 0x6E,
0x74, 0x36, 0x34, // 'UInt64'
0x01, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, // 1 as UInt64
0x03, // LEB128 - column name 'str' has 3 bytes
0x73, 0x74, 0x72, // column name: 'str'
0x06, // LEB128 - column type 'String' has 6 bytes
0x53, 0x74, 0x72,
0x69, 0x6E, 0x67, // 'String'
0x01, // LEB128 - the string has 1 byte
0x31, // '1' as String
]);
简单数据类型
这些较简单数据类型中,单个值的传输格式与 RowBinary/RowBinaryWithNamesAndTypes 类似。
符合这一描述的完整类型列表包括:
- (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
更多详情请参阅上文 "RowBinary 数据类型传输格式" 中对这些类型的说明。
复杂数据类型
以下类型的编码方式与 RowBinary 和 RowBinaryWithNamesAndTypes 不同。
- Nullable
- LowCardinality
- Array
- Map
- Variant
- Dynamic
- JSON
Nullable
在 Native 格式中,Nullable 列在实际数据前会有一段字节,其字节数等于块中的行数。每个字节都表示对应的值是否为 NULL。例如,在这个查询中,每个奇数都会变为 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
输出如下:
const data = new Uint8Array([
// --- Block Header ---
0x01, // LEB128 - 1 column
0x05, // LEB128 - 5 rows
// -- Column Header --
0x0A, // LEB128 - column name has 10 bytes
0x6D, 0x61, 0x79, 0x62, 0x65,
0x5F, 0x6E, 0x75, 0x6C, 0x6C, // column name: 'maybe_null'
0x10, // LEB128 - column type has 16 bytes
0x4E, 0x75, 0x6C, 0x6C,
0x61, 0x62, 0x6C, 0x65,
0x28, 0x55, 0x49, 0x6E,
0x74, 0x36, 0x34, 0x29, // column type: 'Nullable(UInt64)'
// -- Nullable mask --
0x00, // Row 0 is NOT NULL
0x01, // Row 1 is NULL
0x00, // Row 2 is NOT NULL
0x01, // Row 3 is NULL
0x00, // Row 4 is NOT NULL
// -- UInt64 values --
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, // Row 0: 0 as UInt64
// even though we still might have a proper value for this number
// in the block, it should be still returned as NULL to the user!
0x01, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, // Row #1: NULL
0x02, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, // Row #2: 2 as UInt64
0x03, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, // Row #3: NULL, similar to Row #1
0x04, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, // Row #4: 4 as UInt64
]);
对于 Nullable(String),其工作方式也类似。null 标记始终来自 Nullable 掩码字节——
掩码值为 0x01 表示该行是 NULL,与字符串内容无关。对于 NULL 行,
底层字符串会存储为空字符串 (LEB128 长度为 0) 。请注意,非 NULL 的空
字符串其 LEB128 长度同样为 0,因此只有掩码字节才能区分这两种情况。例如,以下查询:
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
输出如下:
const data = new Uint8Array([
// --- Block Header ---
0x01, // LEB128 - 1 column
0x05, // LEB128 - 5 rows
// -- Column Header --
0x09, // LEB128 - column name has 9 bytes
0x6d,
0x61,
0x79,
0x62,
0x65,
0x5f,
0x73,
0x74,
0x72, // column name: 'maybe_str'
0x10, // LEB128 - column type has 16 bytes
0x4e,
0x75,
0x6c,
0x6c,
0x61,
0x62,
0x6c,
0x65,
0x28,
0x53,
0x74,
0x72,
0x69,
0x6e,
0x67,
0x29, // column type: 'Nullable(String)'
// -- Nullable mask --
0x00, // Row 0 is NOT NULL
0x01, // Row 1 is NULL
0x00, // Row 2 is NOT NULL
0x01, // Row 3 is NULL
0x00, // Row 4 is NOT NULL
// -- String values --
0x01,
0x30, // Row 0: LEB128 == 1, '0' as String
0x00, // Row 1: LEB128 == 0, NULL
0x01,
0x32, // Row 2: LEB128 == 1, '2' as String
0x00, // Row 3: LEB128 == 0, NULL
0x01,
0x34, // Row 4: LEB128 == 1, '4' as String
])
LowCardinality
与 RowBinary 中 LowCardinality 为透明表示不同,Native 格式使用基于字典的列式编码。列会被编码为一个版本前缀,随后是唯一值字典,以及一个指向该字典的整型索引数组。
注意
列可以定义为 LowCardinality(Nullable(T)),但不能定义为 Nullable(LowCardinality(T))——这始终会导致服务器报错。
版本前缀是一个值为 1 的 UInt64(LE),每列写入一次。然后,对每个块,会写入以下内容:
UInt64(LE) — IndexesSerializationType 位字段。位 0–7 用于编码索引宽度 (0 = UInt8,1 = UInt16,2 = UInt32,3 = UInt64) 。位 8 (NeedGlobalDictionaryBit) 在 Native 格式中永远不会被设置 (如果遇到它,服务器会抛出异常) 。位 9 表示存在额外的字典键。位 10 表示应重置字典。
UInt64(LE) — 字典键的数量,随后使用内部类型编码对这些键进行批量序列化。
UInt64(LE) — 行数,随后使用相应的 UInt 宽度对索引值进行批量序列化。
字典始终在索引 0 处包含一个默认值 (例如,String 的空字符串、数值类型的 0) 。对于 LowCardinality(Nullable(T)),索引 0 表示 NULL,并且这些键在序列化时不带 Nullable 封装。
例如,LowCardinality(String) 有 5 行 ['foo', 'bar', 'baz', 'foo', 'bar']:
// Version prefix
01 00 00 00 00 00 00 00 // UInt64(LE) = 1
// IndexesSerializationType: UInt8 indexes, has keys, update dictionary
00 06 00 00 00 00 00 00 // UInt64(LE) = 0x0600
04 00 00 00 00 00 00 00 // 4 dictionary keys
00 // key 0: "" (default)
03 66 6f 6f // key 1: "foo"
03 62 61 72 // key 2: "bar"
03 62 61 7a // key 3: "baz"
05 00 00 00 00 00 00 00 // 5 rows
01 02 03 01 02 // indexes → "foo", "bar", "baz", "foo", "bar"
在 LowCardinality(Nullable(String)) 中,索引 0 为 NULL:
01 00 00 00 00 00 00 00 // version
00 06 00 00 00 00 00 00 // IndexesSerializationType
03 00 00 00 00 00 00 00 // 3 keys
00 // key 0: NULL
00 // key 1: "" (default)
03 79 65 73 // key 2: "yes"
05 00 00 00 00 00 00 00 // 5 rows
02 00 02 00 02 // indexes → "yes", NULL, "yes", NULL, "yes"
Array
不同于 RowBinary 中每个数组前都会带有一个 LEB128 元素计数,Native 格式将数组编码为两个列式子流:
- N 个累积的
UInt64 偏移量 (小端序,每个 8 字节) 。第 i 行有 offset[i] - offset[i-1] 个元素,其中 offset[-1] 默认为 0。
- 所有行中的全部嵌套元素会被连续地批量序列化。
例如,Array(UInt32) 有 3 行 [[0, 10], [1, 11], [2, 12]]:
// Offsets
02 00 00 00 00 00 00 00 // 2 (row 0: 2 elements)
04 00 00 00 00 00 00 00 // 4 (row 1: 2 elements)
06 00 00 00 00 00 00 00 // 6 (row 2: 2 elements)
// Nested UInt32 values (6 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
空数组与上一行的偏移量相同。例如,Array(String) 有 4 行 [[], ['0'], ['0','1'], ['0','1','2']]:
00 00 00 00 00 00 00 00 // 0 (empty)
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
Map(K, V) 编码为 Array(Tuple(K, V))——先是数组偏移量,再是所有键,最后是所有值。这与 RowBinary 不同,后者会按每个条目交错存储键和值。
例如,包含 3 行 [{'a':0,'b':10}, {'a':1,'b':11}, {'a':2,'b':12}] 的 Map(String, UInt64):
// Array offsets
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
// All keys (6 Strings)
01 61 // "a"
01 62 // "b"
01 61 // "a"
01 62 // "b"
01 61 // "a"
01 62 // "b"
// All values (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
与 RowBinary 不同,在 RowBinary 中,每一行都会携带自己的判别字节,后面紧跟内联的值;而 Native 格式会将判别信息与数据分离存储。
注意
与 RowBinary 一样,定义中的类型始终按字母顺序排序,判别值是该有序列表中的索引。0xFF (255) 表示 NULL。
Variant 列的编码方式如下:
UInt64(LE) 判别器模式前缀 (0 = BASIC,1 = COMPACT) 。Native 格式输出通常使用 BASIC (0) ;读取以启用 use_compact_variant_discriminators_serialization 方式存储的数据时,可能会出现 COMPACT 模式。
- N 个
UInt8 判别器,每行一个。
- 每种 Variant 类型的数据分别存储为独立的批量列,其中只包含与该类型匹配的行,并按判别值顺序排列。
例如,Variant(String, UInt32) 有 5 行 [0::UInt32, 'hello', NULL, 3::UInt32, 'hello'] (排序后:String = 0,UInt32 = 1) :
00 00 00 00 00 00 00 00 // discriminators mode = BASIC
01 00 ff 01 00 // UInt32, String, NULL, UInt32, String
// String (2 values, rows 1 and 4)
05 68 65 6c 6c 6f // "hello"
05 68 65 6c 6c 6f // "hello"
// UInt32 (2 values, rows 0 and 3)
00 00 00 00 // 0
03 00 00 00 // 3
Dynamic
不同于 RowBinary 中每个值都是自描述的 (类型前缀 + 值) ,Native 格式会将 Dynamic 序列化为结构前缀,后跟一个 Variant 列。
结构前缀包含一个 UInt64(LE) 序列化版本,随后是动态类型的数量 (以 VarUInt 编码) ,接着是各类型的字符串名称。在 V1 版本中,出于兼容性考虑,类型数量会写入两次。后续数据为一个 Variant 列,其类型列表由这些动态类型以及一个内部 SharedVariant 类型组成,并按字母顺序排列。
例如,包含 5 行 [0::UInt32, 'hello', NULL, 3::UInt32, 'hello'] 的 Dynamic:
// Structure prefix (V1)
01 00 00 00 00 00 00 00 // version = V1
02 // num types (V1 writes twice)
02 // num types
06 53 74 72 69 6e 67 // "String"
06 55 49 6e 74 33 32 // "UInt32"
// Variant data: Variant(SharedVariant, String, UInt32)
// discriminants: SharedVariant=0, String=1, UInt32=2
00 00 00 00 00 00 00 00 // discriminators mode = BASIC
02 01 ff 02 01 // UInt32, String, NULL, UInt32, String
// SharedVariant: 0 values
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
与 RowBinary 中每一行都包含路径名和值、因此可自描述不同,Native 格式会以列式结构对 JSON 进行序列化。这种编码方式较为复杂,并且与版本相关:它由一个结构前缀组成,其中包含序列化版本、动态路径名和共享数据布局;随后是类型化路径 (每个路径都作为一个批量列) 、动态路径 (每个路径都作为一个 Dynamic 列) ,以及用于溢出路径的共享数据。
为简化互操作性,可考虑使用设置 output_format_native_write_json_as_string=1,该设置会将 JSON 列序列化为普通的 JSON 文本字符串 (每行一个 String) 。