描述
RowBinary 格式以二进制形式按行解析数据。
各行和值会连续排列,中间没有分隔符。
由于数据采用二进制格式,FORMAT RowBinary 之后的分隔符被严格规定如下:
- 任意数量的空白字符:
' '(空格 - 代码0x20)'\t'(制表符 - 代码0x09)'\f'(换页符 - 代码0x0C)
- 随后必须且只能有一个换行序列:
- Windows 风格
"\r\n" - 或 Unix 风格
'\n'
- Windows 风格
- 后面紧接着是二进制数据。
由于该格式按行处理数据,因此其效率低于 Native 格式。
数据类型的传输格式
无符号 LEB128 (小端序 Base 128)
String、Array 和 Map 等可变大小数据类型的长度。示例实现可参见 LEB128 wiki 页面。
(U)Int8, (U)Int16, (U)Int32, (U)Int64, (U)Int128, (U)Int256
Int8 到 Int256) 采用二进制补码表示。大多数编程语言都支持使用内置工具或常见库从字节数组中提取这类整数。对于 Int128/Int256 和 UInt128/UInt256,由于它们超出了大多数语言的原生整数位宽,可能需要自定义反序列化。
Bool
UInt8 类似。
0表示false1表示true
Float32, Float64
Float32 为 4 字节,Float64 为 8 字节。与整数类似,大多数语言都提供了适合对这些值进行反序列化的工具。
BFloat16
BFloat16 的底层数值示例:
Decimal32, Decimal64, Decimal128, Decimal256
Decimal32- 4 字节,或Int32。Decimal64- 8 字节,或Int64。Decimal128- 16 字节,或Int128。Decimal256- 32 字节,或Int256。
trunc 表示向零截断 (不是向下取整除法,后者对负数的结果会不同) ,而 scale 表示小数点后的位数。例如,对于 Decimal(10, 2) (等价于 Decimal32(2)) ,标度为 2,值 12345 会表示为 (123, 45)。
序列化时需要进行相反的操作:
String
- 一个可变长度整数 (LEB128) ,表示字符串的字节长度。
- 字符串的原始字节。
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 起经过的秒数。
语法:
DateTime 或 DateTime('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 参数定义,参见下方语法:
precision 是 0 到 9 之间的整数。通常只使用以下几个值: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,才能使用 Time 或 Time64。Time 的底层值示例:
Time64
Decimal64 形式存储 (而 Decimal64 本身以 Int64 存储) ,用于表示带有小数秒且精度可配置的时间值。负值也是有效的。
语法:
precision 是 0 到 9 之间的整数。常见值:3 (毫秒) 、6 (微秒) 、9 (纳秒) 。
支持的取值范围:[-999:59:59.xxxxxxxxx, 999:59:59.xxxxxxxxx]。
目前,要使用
Time 或 Time64,必须将设置 enable_time_time64_type 设为 1。Int64 值表示按 10^precision 缩放后的秒的小数部分。
Time64 的底层示例值:
时间间隔类型
Int64 (8 字节,小端序) 。该值表示相应时间单位的数量。负值也是有效的。
时间间隔类型包括:IntervalNanosecond、IntervalMicrosecond、IntervalMillisecond、IntervalSecond、IntervalMinute、IntervalHour、IntervalDay、IntervalWeek、IntervalMonth、IntervalQuarter、IntervalYear。
时间间隔类型名称 (例如
IntervalSecond 与 IntervalDay) 决定了存储值的单位。传输编码始终相同。Enum8, Enum16
Enum8 == Int8) 或双字节 (Enum16 == Int16) ,表示该枚举值在枚举定义中的索引。请注意,存储类型是有符号的——枚举值可以是负数 (例如 Enum8('a' = -128, 'b' = 0)) 。
Enum 可以按如下简单方式定义:
\') ,以及可能出现在带引号字符串中的特殊符号 (如 =) 。
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 个零字节:
IPv4
UInt32 形式存储,占 4 个字节,字节序为小端序。请注意,这不同于 IP 地址通常使用的传统网络字节序 (大端序) 。IPv4 的示例底层值:
IPv6
IPv6 的底层值示例:
Nullable
- 使用一个字节表示该值是否为
NULL:0x00表示该值不是NULL。0x01表示该值是NULL。
- 如果该值不是
NULL,则按常规方式对其底层数据类型进行编码。如果该值是NULL,则不会为底层类型写入任何额外字节。
Nullable(UInt32) 类型的一个值:
LowCardinality
LowCardinality(String) 的编码方式与普通 String 相同。
列可以定义为
LowCardinality(Nullable(T)),但不能定义为 Nullable(LowCardinality(T))——这样定义始终会导致服务器报错。1,以允许在 LowCardinality 中使用大多数数据类型,从而获得更好的覆盖率。
数组
- 一个可变长度整数 (LEB128) ,用于表示数组中的元素个数。
- 数组中的各个元素,编码方式与其底层数据类型相同。
UInt32 值的数组:
数组可以包含可为 NULL 的值,但数组本身不能是 Nullable 类型。
Tuple
Map
Array(Tuple(K, V)),其中 K 为键类型,V 为值类型。Map 的编码方式如下:
- 一个变长整数 (LEB128) ,用于表示 Map 中的元素个数。
- Map 中的元素以键值对的形式存储,并按各自对应的类型进行编码。
String、值为 UInt32 的 Map:
也可以使用具有深层嵌套结构的 Map,例如
Map(String, Map(Int32, Array(Nullable(String)))),其编码方式与上文所述类似。Variant
Variant(T1, T2, ..., TN) 表示此类型的每一行都可以是 T1、T2、……、TN 中的任一种类型的值,也可以都不是 (即 NULL 值) 。
请看下面的示例:
NULL 值会使用值为 0xFF 的判别值字节进行编码:
Variant 类型。
Dynamic
Dynamic 类型可以保存任意类型的值,具体类型在运行时确定。在 RowBinary format 中,每个值都是自描述的:第一部分是以这种格式表示的类型说明。随后是具体内容,其值编码方式如本文档所述。因此,要解析某个值,你只需使用类型索引来确定合适的解析器,然后复用你在其他地方已有的 RowBinary 解析逻辑。
BinaryTypeIndex 是用于标识类型的单字节。有关类型索引和参数,请参见此处的参考文档。
NULL Dynamic 值使用 BinaryTypeIndex 0x00 (即 Nothing 类型) 编码,不包含任何额外字节:
JSON
- 类型化路径 - 在 schema 中声明并显式指定类型的路径 (例如:
JSON(user_id UInt32, name String)) - 超出动态路径限制时的动态路径/溢出路径 - 运行时发现并以
Dynamic类型存储的路径。其值编码前会先写入类型定义。
路径按顺序分三组序列化:类型化路径、动态路径,以及共享数据 (溢出) 路径。类型化路径和动态路径按实现定义的顺序写入 (由内部哈希映射迭代决定) ,共享数据路径则按字母顺序写入。读取方不应依赖任何特定的路径顺序。反序列化器按路径名称而非位置进行分发。
RowBinary 格式中的每个 JSON 行将被序列化为:
JSON(user_id UInt32, active Bool)
行:{"user_id": 42, "active": true}
二进制编码 (十六进制及注释) :
JSON(user_id UInt32, active Bool)
行:{"user_id": 42, "active": true, "name": "Alice"}
二进制编码 (十六进制及注释) :
JSON(score Nullable(Int32))
行:{"score": null }
二进制编码 (十六进制及注释) :
JSON(name String)
行:{"name": null}
二进制编码:
JSON(id UInt64)
行: {"id": 100, "metadata": 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 类型
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)))。
RowBinaryWithNamesAndTypes 格式的头部将包含这些类型的别名,例如 Point、Ring、Polygon、MultiPolygon、LineString 和 MultiLineString。
Geometry
Geometry 是一种 Variant 类型,可容纳上文列出的任意 Geo 类型。在传输格式中,它的编码方式与 Variant 完全相同,通过一个判别值字节指示后续的是哪种 geo 类型。
Geometry 的判别值索引如下:
传输格式结构:
Point 编码为 Geometry 的示例:
Ring 编码为 Geometry 的示例:
Nested
Nested 的传输格式取决于 flatten_nested 设置。
flatten_nested = 1 (默认)
Nested 会被展平为多个独立数组。每个子列都会成为一个单独的 Array 列,列名以点号分隔:
DESCRIBE TABLE foo 会显示扁平化后的列:
flatten_nested = 0
flatten_nested = 0 时,Nested 会保留为一个类型为 Array(Tuple(...)) 的单独列。列名不使用点号分隔:
DESCRIBE TABLE foo 显示一个列:
Array(Tuple(String, Int32)):先是数组长度前缀,然后依次写入每个元素的 Tuple 字段:
SimpleAggregateFunction
SimpleAggregateFunction(func, T) 的编码与其底层数据类型 T 完全一致。聚合函数名称不会影响传输格式。
例如,SimpleAggregateFunction(max, UInt32) 的编码方式与普通的 UInt32 相同:
SimpleAggregateFunction(max, UInt32),但实际传输的值只是 UInt32:
AggregateFunction
AggregateFunction(func, T) 存储聚合函数的完整中间状态。与 SimpleAggregateFunction 不同,后者也存储中间状态,但其编码方式与底层 Data type 完全一致;AggregateFunction 存储的是不透明的二进制 blob,其格式因具体聚合函数而异。
内部格式因函数而异。下面是几个简单示例:
countState — 将计数存储为 VarUInt (LEB128) :
sumState — 将累加和存储为定长整数。其位宽取决于参数类型 (整型参数为 UInt64) :
minState / maxState — 存储一个标志字节,后面跟着底层类型的值。空状态 (未见到任何值) 时,标志为 0x00;存在值时,标志为 0x01:
uniq、quantile 或 groupArray 等较复杂的函数使用特定于其实现的格式。如果你需要读取或写入这些状态,请查阅 ClickHouse 中相应函数的源代码。QBit
QBit 是一种向量类型,可在不同精度级别下实现高效查找。它在内部以转置格式存储。在传输格式中,QBit 只是由底层元素类型 (Int8、Float32、Float64 或 BFloat16) 组成的 Array。用于存储的位转置优化是在服务端完成的,而不是在 RowBinary 协议中完成的。
语法:
element_type 为 Int8、Float32、Float64 或 BFloat16,dimension 为固定的向量维度。可选的 stride 仅控制位平面在服务端如何分组到存储流中;它不会影响 RowBinary 传输格式,后者始终是由 dimension 个元素组成的完整数组。
传输格式:与 Array(element_type) 完全相同:
QBit(Float32, 4) 对 [1.0, 2.0, 3.0, 4.0] 的编码示例:
格式设置
RowBinary 类型的格式。