Skip to main content

描述

RowBinary 格式以二进制形式按行解析数据。 各行和值会连续排列,中间没有分隔符。 由于数据采用二进制格式,FORMAT RowBinary 之后的分隔符被严格规定如下:
  • 任意数量的空白字符:
    • ' ' (空格 - 代码 0x20)
    • '\t' (制表符 - 代码 0x09)
    • '\f' (换页符 - 代码 0x0C)
  • 随后必须且只能有一个换行序列:
    • Windows 风格 "\r\n"
    • 或 Unix 风格 '\n'
  • 后面紧接着是二进制数据。
由于该格式按行处理数据,因此其效率低于 Native 格式。

数据类型的传输格式

示例中提供的大多数查询都可以使用 curl 执行,并将输出保存到文件中。
然后,可以用十六进制编辑器查看数据。

无符号 LEB128 (小端序 Base 128)

这是一种无符号小端序可变长度整数编码,用于编码 StringArrayMap 等可变大小数据类型的长度。示例实现可参见 LEB128 wiki 页面

(U)Int8, (U)Int16, (U)Int32, (U)Int64, (U)Int128, (U)Int256

所有整数类型都会使用适当数量的字节按小端序编码。有符号类型 (Int8Int256) 采用二进制补码表示。大多数编程语言都支持使用内置工具或常见库从字节数组中提取这类整数。对于 Int128/Int256UInt128/UInt256,由于它们超出了大多数语言的原生整数位宽,可能需要自定义反序列化。

Bool

布尔值编码为单个字节,其反序列化方式与 UInt8 类似。
  • 0 表示 false
  • 1 表示 true

Float32, Float64

小端序浮点数,Float32 为 4 字节,Float64 为 8 字节。与整数类似,大多数语言都提供了适合对这些值进行反序列化的工具。

BFloat16

BFloat16 (Brain Floating Point) 是一种 16 位浮点格式,具有 Float32 的取值范围,但精度较低,因此适合机器学习负载。其传输格式本质上就是 Float32 值的高 16 位。如果你的编程语言不原生支持它,最简单的处理方式是按 UInt16 读写,再与 Float32 相互转换: 将 BFloat16 转换为 Float32 (伪代码) :
将 Float32 转换为 BFloat16 (伪代码如下) :
BFloat16 的底层数值示例:

Decimal32, Decimal64, Decimal128, Decimal256

Decimal 类型以具有相应位宽的 小端序 整数表示。
  • Decimal32 - 4 字节,或 Int32
  • Decimal64 - 8 字节,或 Int64
  • Decimal128 - 16 字节,或 Int128
  • Decimal256 - 32 字节,或 Int256
反序列化 Decimal 值时,可以使用以下伪代码得出整数部分和小数部分:
其中,trunc 表示向零截断 (不是向下取整除法,后者对负数的结果会不同) ,而 scale 表示小数点后的位数。例如,对于 Decimal(10, 2) (等价于 Decimal32(2)) ,标度为 2,值 12345 会表示为 (123, 45) 序列化时需要进行相反的操作:
更多详情,请参见 ClickHouse 文档中的 Decimal 类型

String

ClickHouse 字符串是任意的字节序列。它们不必是有效的 UTF-8。长度前缀表示的是字节长度,而不是字符数。 编码由两部分组成:
  1. 一个可变长度整数 (LEB128) ,表示字符串的字节长度。
  2. 字符串的原始字节。
例如,字符串 foobar 会按如下方式编码,共使用 七个 字节:

FixedString

String 不同,FixedString 的长度是固定的,并在 schema 中定义。如果值短于 N,它会被编码为一个字节序列,并在末尾用零字节填充。
读取 FixedString 时,末尾的零字节既可能是填充字节,也可能是数据中实际存在的 \0 字符;在线路上传输时两者无法区分。ClickHouse 本身会原样保留全部 N 个字节。
空的 FixedString(3) 只包含用于填充的零字节:
包含字符串 hi 的非空 FixedString(3)
包含字符串 bar 的非空 FixedString(3)
在最后一个示例中,不需要填充,因为 三个 字节都已用上。

Date

存储为 UInt16 (两个字节) ,表示自 1970-01-01 起的天数。 支持的取值范围:[1970-01-01, 2149-06-06] Date 的底层示例值:

Date32

Int32 (4 字节) 存储,表示相对于 1970-01-01 之前或之后 的天数。 支持的取值范围:[1900-01-01, 2299-12-31] Date32 的底层值示例:
早于纪元的日期:

DateTime

存储为 UInt32 (四个字节) ,表示自 1970-01-01 00:00:00 UTC 起经过的秒数。 语法:
例如,DateTimeDateTime('UTC')
二进制值始终是相对于 UTC 纪元的偏移量。时区不会改变编码。不过,时区确实会影响字符串值在插入时的解释方式:将 '2024-01-15 10:30:00' 插入 DateTime('America/New_York') 列时,存储的纪元值会不同于将同一个字符串插入 DateTime('UTC') 列时的结果,因为该字符串会按列的时区解释为本地时间。在传输中,它们都只是 UInt32 类型的纪元秒。
支持的值范围:[1970-01-01 00:00:00, 2106-02-07 06:28:15] DateTime 的底层值示例:

DateTime64

存储为 Int64 (8 字节) ,表示相对于 1970-01-01 00:00:00 UTC 之前或之后tick 数。tick 的分辨率由 precision 参数定义,参见下方语法:
其中,precision09 之间的整数。通常只使用以下几个值:3 (毫秒) 、6 (微秒) 、 9 (纳秒) 。 有效的 DateTime64 定义示例包括:DateTime64(0)DateTime64(3)DateTime64(6, 'UTC')DateTime64(9, 'Europe/Amsterdam')
DateTime 一样,其二进制值始终是相对于 UTC 纪元 的偏移量。时区会影响插入时字符串值的解释方式 (参见 DateTime 说明) ,但编码本身始终是自 UTC 纪元 起算的 Int64 tick 值。
DateTime64 类型的底层 Int64 值,可以理解为 UNIX 纪元 之前或之后按下列单位计数的数量:
  • DateTime64(0) - 秒。
  • DateTime64(3) - 毫秒。
  • DateTime64(6) - 微秒。
  • DateTime64(9) - 纳秒。
支持的取值范围:[0000-01-01 00:00:00, 9999-12-31 23:59:59.999999999] (适用于精度最高为 7 的情况;精度 8 和 9 的范围更窄,见下方说明)。 DateTime64 的底层值示例:
  • DateTime64(3):值 1546300800000 表示 2019-01-01 00:00:00 UTC
  • DateTime64(6):值 1705314600123456 表示 2024-01-15 10:30:00.123456 UTC
  • DateTime64(9):值 1705314600123456789 表示 2024-01-15 10:30:00.123456789 UTC
由于底层 Int64 tick 的取值范围在更高精度下会更窄,因此支持的最大值也会变小:精度为 8 时为 4892-10-07,精度为 9 (纳秒) 时为 UTC 时间 2262-04-11 23:47:16

Time

存储为 Int32,表示以秒为单位的时间值。负值有效。 支持的取值范围:[-999:59:59, 999:59:59] (即 [-3599999, 3599999] 秒) 。
目前,必须将设置 enable_time_time64_type 设为 1,才能使用 TimeTime64
Time 的底层值示例:

Time64

在内部,Time64 以 Decimal64 形式存储 (而 Decimal64 本身以 Int64 存储) ,用于表示带有小数秒且精度可配置的时间值。负值也是有效的。 语法:
其中,precision09 之间的整数。常见值:3 (毫秒) 、6 (微秒) 、9 (纳秒) 。 支持的取值范围:[-999:59:59.xxxxxxxxx, 999:59:59.xxxxxxxxx]
目前,要使用 TimeTime64,必须将设置 enable_time_time64_type 设为 1
底层的 Int64 值表示按 10^precision 缩放后的秒的小数部分。 Time64 的底层示例值:

时间间隔类型

所有时间间隔类型都存储为 Int64 (8 字节,小端序) 。该值表示相应时间单位的数量。负值也是有效的。 时间间隔类型包括:IntervalNanosecondIntervalMicrosecondIntervalMillisecondIntervalSecondIntervalMinuteIntervalHourIntervalDayIntervalWeekIntervalMonthIntervalQuarterIntervalYear
时间间隔类型名称 (例如 IntervalSecondIntervalDay) 决定了存储值的单位。传输编码始终相同。
底层值示例:

Enum8, Enum16

存储为单字节 (Enum8 == Int8) 或双字节 (Enum16 == Int16) ,表示该枚举值在枚举定义中的索引。请注意,存储类型是有符号的——枚举值可以是负数 (例如 Enum8('a' = -128, 'b' = 0)) 。 Enum 可以按如下简单方式定义:
上面定义的 Enum8 在客户端中对应的值如下:
或者采用更复杂的方式,例如这样:
上面定义的 Enum16 在客户端上映射为以下值:
对于数据类型解析器来说,主要难点在于识别枚举定义中的转义符号 (如 \') ,以及可能出现在带引号字符串中的特殊符号 (如 =) 。

UUID

表示为一个由 16 个字节构成的序列。UUID 以 两个小端序 UInt64 存储:标准 UUID 表示中的前 8 个字节会进行字节倒序,后 8 个字节也会分别进行字节倒序。 例如,给定 UUID 61f0c404-5cb3-11e7-907b-a6006ad3dba0
  • 标准字节表示:61 f0 c4 04 5c b3 11 e7 | 90 7b a6 00 6a d3 db a0
  • 前半部分倒序后 (LE UInt64) :e7 11 b3 5c 04 c4 f0 61
  • 后半部分倒序后 (LE UInt64) :a0 db d3 6a 00 a6 7b 90
UUID 的底层表示值示例:
  • 61f0c404-5cb3-11e7-907b-a6006ad3dba0 表示为:
  • 默认的 UUID 00000000-0000-0000-0000-000000000000 表示为 16 个零字节:
当插入新记录但未指定 UUID 值时,可以使用它。

IPv4

UInt32 形式存储,占 4 个字节,字节序为小端序。请注意,这不同于 IP 地址通常使用的传统网络字节序 (大端序) 。IPv4 的示例底层值:

IPv6

以 16 个字节存储,采用 大端序 / 网络字节序 (MSB 优先) 。IPv6 的底层值示例:

Nullable

Nullable 数据类型的编码方式如下:
  1. 使用一个字节表示该值是否为 NULL
    • 0x00 表示该值不是 NULL
    • 0x01 表示该值是 NULL
  2. 如果该值不是 NULL,则按常规方式对其底层数据类型进行编码。如果该值是 NULL,则不会为底层类型写入任何额外字节
例如,Nullable(UInt32) 类型的一个值:

LowCardinality

在 RowBinary format 中,低基数标记不会影响传输格式。例如,LowCardinality(String) 的编码方式与普通 String 相同。
这仅适用于 RowBinary。在 Native format 中,LowCardinality 使用的是另一种基于字典的编码方式。
列可以定义为 LowCardinality(Nullable(T)),但不能定义为 Nullable(LowCardinality(T))——这样定义始终会导致服务器报错。
在测试时,可以将 allow_suspicious_low_cardinality_types 设置为 1,以允许在 LowCardinality 中使用大多数数据类型,从而获得更好的覆盖率。

数组

数组按以下方式编码:
  1. 一个可变长度整数 (LEB128) ,用于表示数组中的元素个数。
  2. 数组中的各个元素,编码方式与其底层数据类型相同。
例如,一个包含 UInt32 值的数组:
一个稍复杂一点的示例:
数组可以包含可为 NULL 的值,但数组本身不能是 Nullable 类型。
以下是有效的:
其编码方式如下:
有关如何处理多维数组的示例,请参见 Geo 部分

Tuple

Tuple 的编码方式是:其所有元素按顺序依次排列,每个元素都采用各自对应的传输格式,且不包含任何额外的元信息或分隔符。
Tuple 数据类型的字符串编码与 Enum 类型 面临类似的挑战,例如需要处理转义符号和特殊字符;而对于 Tuple,还需要额外处理左括号和右括号。此外,请注意,较为复杂的 Tuple 还可能包含其他嵌套的 Tuple、Array、Map,甚至枚举。 例如,在下表中,这个 Tuple 包含一个名称中带有反引号和括号的枚举;如果处理不当,可能会导致解析问题:

Map

Map 可视为 Array(Tuple(K, V)),其中 K 为键类型,V 为值类型。Map 的编码方式如下:
  1. 一个变长整数 (LEB128) ,用于表示 Map 中的元素个数。
  2. Map 中的元素以键值对的形式存储,并按各自对应的类型进行编码。
例如,一个键为 String、值为 UInt32 的 Map:
也可以使用具有深层嵌套结构的 Map,例如 Map(String, Map(Int32, Array(Nullable(String)))),其编码方式与上文所述类似。

Variant

该类型表示其他数据类型的 union。类型 Variant(T1, T2, ..., TN) 表示此类型的每一行都可以是 T1T2、……、TN 中的任一种类型的值,也可以都不是 (即 NULL 值) 。
虽然对最终用户来说,Variant(T1, T2)Variant(T2, T1) 的含义完全相同,但对于传输格式而言,定义中类型的顺序很重要:定义中的类型始终按字母顺序排序。这一点很关键,因为具体的变体是通过“判别值”编码的——也就是该数据类型在定义中的索引。
请看下面的示例:
NULL 值会使用值为 0xFF 的判别值字节进行编码:
allow_suspicious_variant_types 设置可用于更充分地测试 Variant 类型。

Dynamic

Dynamic 类型可以保存任意类型的值,具体类型在运行时确定。在 RowBinary format 中,每个值都是自描述的:第一部分是以这种格式表示的类型说明。随后是具体内容,其值编码方式如本文档所述。因此,要解析某个值,你只需使用类型索引来确定合适的解析器,然后复用你在其他地方已有的 RowBinary 解析逻辑。
其中,BinaryTypeIndex 是用于标识类型的单字节。有关类型索引和参数,请参见此处的参考文档。 NULL Dynamic 值使用 BinaryTypeIndex 0x00 (即 Nothing 类型) 编码,不包含任何额外字节:
示例:

JSON

JSON 类型将数据编码为以下两个不同的类别:
  1. 类型化路径 - 在 schema 中声明并显式指定类型的路径 (例如:JSON(user_id UInt32, name String))
  2. 超出动态路径限制时的动态路径/溢出路径 - 运行时发现并以 Dynamic 类型存储的路径。其值编码前会先写入类型定义。
这两类的传输格式和规则各不相同。 路径按顺序分三组序列化:类型化路径、动态路径,以及共享数据 (溢出) 路径。类型化路径和动态路径按实现定义的顺序写入 (由内部哈希映射迭代决定) ,共享数据路径则按字母顺序写入。读取方不应依赖任何特定的路径顺序。反序列化器按路径名称而非位置进行分发。 RowBinary 格式中的每个 JSON 行将被序列化为:
示例: 1. 仅含类型化路径的简单 JSON: Schema: JSON(user_id UInt32, active Bool) 行:{"user_id": 42, "active": true} 二进制编码 (十六进制及注释) :
2. 包含类型化路径和动态路径的简单 JSON: Schema: JSON(user_id UInt32, active Bool) 行:{"user_id": 42, "active": true, "name": "Alice"} 二进制编码 (十六进制及注释) :
3. NULL 值处理: 对于带类型的 Nullable 列,结果为 null: Schema: JSON(score Nullable(Int32)) 行:{"score": null } 二进制编码 (十六进制及注释) :
对于有类型的非可空列,将返回默认值: Schema: JSON(name String) 行:{"name": null} 二进制编码:
对于动态路径,该设置将被忽略: Schema: JSON(id UInt64) 行: {"id": 100, "metadata": null} 二进制编码:
注意:带有 NULL 值的 metadata 路径不会被包含,因为动态路径仅在非空时才会被序列化。这是与类型化路径的一个关键区别。 4. 嵌套 JSON 对象: Schema: JSON() 行: {"user": {"name": "Bob", "age": 30}} 二进制编码 (带标注的十六进制) :
注意:嵌套对象会被展平为以点分隔的路径 (例如使用 user.name,而不是嵌套结构) 。 替代方案:JSON 作为 String 模式 设置 output_format_binary_write_json_as_string=1 后,JSON 列会被序列化为单个 JSON 文本字符串,而不是结构化的二进制格式。对于写入 JSON 列,也有对应的设置 input_format_binary_read_json_as_string。这里选择哪种设置,取决于你希望在客户端还是服务端解析 JSON。

Geo 类型

Geo 是一类用于表示地理数据的数据类型,包括:
  • Point - 表示为 Tuple(Float64, Float64)
  • Ring - 表示为 Array(Point),或 Array(Tuple(Float64, Float64))
  • Polygon - 表示为 Array(Ring),或 Array(Array(Tuple(Float64, Float64)))
  • MultiPolygon - 表示为 Array(Polygon),或 Array(Array(Array(Tuple(Float64, Float64))))
  • LineString - 表示为 Array(Point),或 Array(Tuple(Float64, Float64))
  • MultiLineString - 表示为 Array(LineString),或 Array(Array(Tuple(Float64, Float64)))
Geo 值的传输格式与 Tuple 和 Array 完全相同。RowBinaryWithNamesAndTypes 格式的头部将包含这些类型的别名,例如 PointRingPolygonMultiPolygonLineStringMultiLineString

Geometry

Geometry 是一种 Variant 类型,可容纳上文列出的任意 Geo 类型。在传输格式中,它的编码方式与 Variant 完全相同,通过一个判别值字节指示后续的是哪种 geo 类型。 Geometry 的判别值索引如下: 传输格式结构:
Point 编码为 Geometry 的示例:
Ring 编码为 Geometry 的示例:

Nested

Nested 的传输格式取决于 flatten_nested 设置。
单行中的所有 component 数组长度必须相同。这是由 server 强制执行的约束。长度不一致会导致插入错误。

flatten_nested = 1 (默认)

在默认设置下,Nested 会被展平为多个独立数组。每个子列都会成为一个单独的 Array 列,列名以点号分隔:
DESCRIBE TABLE foo 会显示扁平化后的列:
每个数组都会单独序列化,如 Array 一节所述:

flatten_nested = 0

flatten_nested = 0 时,Nested 会保留为一个类型为 Array(Tuple(...)) 的单独列。列名不使用点号分隔:
DESCRIBE TABLE foo 显示一个列:
编码为 Array(Tuple(String, Int32)):先是数组长度前缀,然后依次写入每个元素的 Tuple 字段:
请注意,这里的各字段是按元素交错排列的 (a₁, b₁, a₂, b₂) ,而不是像扁平化表示那样按列分组排列 (a₁, a₂, b₁, b₂) 。

SimpleAggregateFunction

SimpleAggregateFunction(func, T) 的编码与其底层数据类型 T 完全一致。聚合函数名称不会影响传输格式。 例如,SimpleAggregateFunction(max, UInt32) 的编码方式与普通的 UInt32 相同:
RowBinaryWithNamesAndTypes 请求头将类型标示为 SimpleAggregateFunction(max, UInt32),但实际传输的值只是 UInt32

AggregateFunction

AggregateFunction(func, T) 存储聚合函数的完整中间状态。与 SimpleAggregateFunction 不同,后者也存储中间状态,但其编码方式与底层 Data type 完全一致;AggregateFunction 存储的是不透明的二进制 blob,其格式因具体聚合函数而异。
聚合状态在 RowBinary 中没有长度前缀。parser 必须了解每个具体聚合函数的内部 serialization 格式,才能知道应读取多少字节。实际使用中,大多数客户端都会将聚合状态视为不透明对象,并使用 *State / *Merge 组合器,由 server 处理 serialization。
内部格式因函数而异。下面是几个简单示例: countState — 将计数存储为 VarUInt (LEB128) :
sumState — 将累加和存储为定长整数。其位宽取决于参数类型 (整型参数为 UInt64) :
minState / maxState — 存储一个标志字节,后面跟着底层类型的值。空状态 (未见到任何值) 时,标志为 0x00;存在值时,标志为 0x01
空状态 (未聚合任何行) :
uniqquantilegroupArray 等较复杂的函数使用特定于其实现的格式。如果你需要读取或写入这些状态,请查阅 ClickHouse 中相应函数的源代码。

QBit

QBit 是一种向量类型,可在不同精度级别下实现高效查找。它在内部以转置格式存储。在传输格式中,QBit 只是由底层元素类型 (Int8Float32Float64BFloat16) 组成的 Array。用于存储的位转置优化是在服务端完成的,而不是在 RowBinary 协议中完成的。 语法:
其中,element_typeInt8Float32Float64BFloat16dimension 为固定的向量维度。可选的 stride 仅控制位平面在服务端如何分组到存储流中;它不会影响 RowBinary 传输格式,后者始终是由 dimension 个元素组成的完整数组。 传输格式:与 Array(element_type) 完全相同:
QBit(Float32, 4)[1.0, 2.0, 3.0, 4.0] 的编码示例:

格式设置

以下设置适用于所有 RowBinary 类型的格式。
最后修改于 2026年7月23日