Delta Lake Collated String Type 协议详解:字符串排序规则、collations 表特性与按排序规则统计的文件跳过
【免费下载链接】deltaAn open-source storage framework that enables building a Lakehouse architecture with compute engines including Spark, PrestoDB, Flink, Trino, and Hive and APIs项目地址: https://gitcode.com/GitHub_Trending/del/delta
Delta Lake 默认对所有字符串按 UTF-8 二进制编码进行等值比较与排序,而本仓库 protocol_rfcs/collated-string-type.md 提出了一项协议级增强:为字符串列引入可选的 collation(排序规则,如大小写不敏感比较、按语言区域排序),并让 per-file 列统计与排序规则版本关联,从而在保证正确性的前提下继续支持文件级数据跳过(data skipping)。读完本文,你将掌握 collations 表特性的启用前提(Writer Version 7、collations与domainMetadata两个 writer feature)、collation 标识符的三段式格式、__COLLATIONS在表 schema 中的存储编码方式,以及statsWithCollation统计结构的读写要求。
一、为什么需要 Collations:字符串比较规则被"固化"在协议层
在 Delta Lake 中,所有字符串都以 UTF-8 编码存储,默认的二进制 collation 意味着:两个字符串相等当且仅当其 UTF-8 字节序列相同,排序也直接按字节序进行。这种设计简单、确定,但对真实业务并不总是友好,例如:
- 大小写不敏感查找:
"Delta"与"delta"按二进制 collation 不等,需要引擎额外转换或依赖自定义逻辑; - 语言区域排序:德语、土耳其语等语言对字母排序(如变音符号、
i/ı)有不同于字节序的规则; - 文件跳过失效:当查询条件使用了与写入统计时不同的比较规则时,用旧规则收集的 min/max 统计可能给出错误的下界/上界,导致数据跳过产生错误结果。
Collated String Type 协议改变正是为了解决这些问题的:它允许在表 schema 中为字符串列声明 collation,让读者按 schema 中的规则进行比较与排序;同时让每个版本的 collation 拥有独立的列统计,从而保证基于统计的文件跳过依然精确。
该协议变更由三部分组成(见 RFC 开头):
- 在表 schema 中声明 collations;
- Per-column statistics 用产生它们的 collation 进行标注;
- 使用 domain metadata 记录"活跃的 collation 版本"。
需要强调的是,collation 只影响比较与排序,不改变字符串的存储方式。列仍以 UTF-8 字节序列存储,只是相等判断和排序顺序可以按规则变化。
二、启用前提:Collations Table Feature 与 Writer Version 7
RFC 明确给出了启用collations表特性必须同时满足的三个条件:
- 表的Writer Version 必须为 7;
- 表的
writerFeatures中必须包含collations; - 表的
writerFeatures中必须包含domainMetadata(用于存放 collation 版本提示)。
在仓库的 kernel 实现中,可以找到与 RFC 完全对应的特性定义。见 TableFeatures.java:COLLATIONS_PREVIEW_W_FEATURE(名为collations-preview)被注释为"被其稳定版本collations取代",而COLLATIONS_W_FEATURE对应Collations类,其构造为super("collations", /* minWriterVersion = */ 7),是一个writer-only 特性——这与 RFC 中"collations表特性是 writer only 特性"的描述完全一致。
值得注意的是"writer-only"的深层含义:不支持 collations 的客户端仍然可以读取表,只是它们必须退回 UTF-8 二进制 collation 来解读字符串;只有写入端才必须感知该特性。另外,收集 collated 统计是可选的——即使某字段声明了非二进制 collation,写入端也可以只提供 UTF-8 二进制 collation 的统计。
三、Reader 要求:谁允许用哪份统计做文件跳过
RFC 为读者定义了三条约束:
- 当表的
writerFeatures包含collations时,读者可以依据 schema 中声明的 collation 对字符串做等值比较与排序; - 若某个字符串类型未声明 collation,读者必须使用 UTF-8 二进制表示的默认比较运算符;
- 文件跳过必须严格匹配 collation 及其版本:只有当过滤操作符显式指定列使用某个 collation 时,才允许用该 collation 收集的列统计做跳过。RFC 举例说明:当使用配置为
ICU.en_US.72的等值比较操作符过滤字符串列时,读者不得使用spark.UTF8_LCASE.75.1的统计做跳过,也不得使用ICU.en_US.69的统计——因为 collation 版本号不一致。
这一条是正确性的关键:min/max 值会随 collation 及版本不同而变化,混用版本可能导致错误跳过数据。RFC 在结尾还补充了一条通用约束:引擎可以依据自身的 collation 优先级规则,对操作实际应用与 schema 不同的 collation,但只有当用于跳过的统计 collation 与过滤操作所用 collation 在所有方面(包括版本号)完全一致时,才可使用该统计。
在 Spark 侧的统计读取实现中,collation 版本确实参与了统计路径的定位。DataSkippingReader.scala 的getStatsColumnOpt注释明确写道:对于 collated 字符串的统计,"该路径包含带版本的 collation 标识符"(the path contains the versioned collation identifier),且路径按逆序存储;方法会先校验路径在 stats schema 中是否存在,若统计类型不存在(如未收集统计或禁用了列统计)则返回None。
四、Writer 要求:写 schema 元数据、写统计、维护版本提示
RFC 对写入端提出五条要求:
- 对使用非默认 collation(即不是按 UTF-8 二进制比较)的列,必须在 schema metadata 中写入 collation 标识符;
- 对使用默认 collation(UTF-8 二进制比较)的列,不得写入 collation 标识符;
- 对使用非默认 collation 的字符串列,可以在
statsWithCollation中写 per-file 统计(详见 PROTOCOL.md 的 Per-file Statistics 一节); - 若写入端为某个新版本的 collation 新增了 per-file 统计,应同步更新
collations表特性的domainMetadata,把用于收集统计的新 collation 版本加入其中; - 若某个 collation 版本不再需要收集统计(例如引擎升级了 ICU 库、改用更新的版本),可以从
domainMetadata中移除该版本。
第 4、5 条体现了一种"协作式"的版本管理:domainMetadata里的writeVersions只是一个提示(hint),帮助客户端在写入时选择合适的 collation 版本,而不必扫描所有 AddFile 的统计;RFC 同时声明客户端允许忽略这些提示。
五、Collation 标识符:Provider.Name[.Version] 三段式
Collation 通过标识符引用。Delta 协议本身除了二进制 collation 外不规定任何具体的排序规则,但它支持 provider 的概念,引擎可以使用 ICU 之类的 provider 并在统计中做相应标注。
标识符由三个部分用点号连接而成:
| 部分 | 说明 | |-|-| | Provider(提供者) | provider 的名称,不允许包含点号| | Name(名称) | provider 提供的 collation 名称,不允许包含点号| | Version(版本) | 版本字符串,允许包含点号;此部分可选。不带版本的 collation 用于 schema 中,因为读者不被强制使用某个特定版本;统计则必须使用带版本的 collation 标注以保证正确性 |
仓库 kernel API 中 CollationIdentifier.java 是这一格式的直接实现:
- 默认常量
SPARK_UTF8_BINARY = new CollationIdentifier("SPARK", "UTF8_BINARY"),即 Spark 默认的 UTF-8 二进制 collation; - 构造时 provider、name、version 都会转为大写,且 version 可为空(
Optional<String>); fromString(String identifier)按点号个数解析:1 个点表示PROVIDER.NAME,2 个及以上表示PROVIDER.NAME.VERSION(使用split("\\.", 3)保证版本里的点不被拆散);toString()输出PROVIDER.NAME[.VERSION],toStringWithoutVersion()输出PROVIDER.NAME;equals要求 provider、name、version 三者全部相同,这与 RFC 中"统计只能被相同 collation(含版本)复用"的语义一致。
例如:ICU.de_DE(无版本,用于 schema)、ICU.en_US.72(带版本,用于统计标注)、spark.UTF8_LCASE.75.1(Spark 提供的大小写不敏感 collation,版本 75.1)。
在 kernel 的类型系统中,StringType直接携带 collation。StringType.java 中,StringType.STRING常量以SPARK_UTF8_BINARY为默认 collation;也提供了StringType(CollationIdentifier)与StringType(String collationName)两个构造器。值得注意的实现细节是:写路径的兼容性检查忽略 collation 差异——isWriteCompatible只要求目标也是StringType,而equivalent也只看类型是否为 string;这意味着写入时不会因 collation 不同而拒绝数据。
六、在表 schema 中声明 Collations:__COLLATIONS元数据键
6.1 存储位置与编码规则
Collations 可以为 schema 中的任何字符串类型指定,范围包括:
- 字符串字段本身;
- map 的 key 与 value 类型;
- array 的 element 类型。
它们存储在最近的祖先 StructField的 metadata 的__COLLATIONS键中。对于嵌套的 map/array,其路径编码方式与 IcebergCompatV2 中的 id 编码方式相同(以点号连接嵌套路径,如col2.element.key)。Collation 标识符在 schema 中不带版本存储,因为读者读取时不被强制使用特定版本。
6.2 完整示例:从数据 schema 到带 collation 的 JSON schema
RFC 给出了如下数据 schema 示例(省略无关字段):
|-- col1: string |-- col2: array | |-- elementType: map | |-- keyType: string | |-- valueType: struct | |-- f1: string对应的、带 collation 信息的 JSON schema 如下:
{ "type":"struct", "fields":[ { "name":"col1", "type":"string", "metadata":{ "__COLLATIONS":{ "col1":"ICU.de_DE" } } }, { "name":"col2", "type":{ "type":"array", "elementType":{ "type":"map", "keyType":"string", "valueType":{ "type":"struct", "fields":[ { "name":"f1", "type":"string", "metadata":{ "__COLLATIONS":{ "f1":"ICU.de_DE" } } } ] } } }, "metadata":{ "__COLLATIONS":{ "col2.element.key":"ICU.en_US" } } } ] }观察这个示例可以得出三条实用结论:
- 普通字符串字段(
col1):__COLLATIONS直接写在该字段自身的 metadata 中,值为无版本的ICU.de_DE; - 嵌套结构中的字符串字段(
col2.element.value.f1):__COLLATIONS写在f1字段(其最近的祖先 StructField)的 metadata 中,值同样为ICU.de_DE; - 嵌套 map/array 中的 key/value/element 字符串(
col2.element.key):__COLLATIONS写在最外层承载该结构的字段(col2)的 metadata 中,键使用点号路径表示嵌套位置(col2.element.key),与 IcebergCompatV2 的 id 编码风格一致。
6.3 对协议表的更新
RFC 还同步要求更新协议文档中的两张表:
- Primitive Types 表中的 string 行更新为:
UTF-8 编码的字符序列。可以在 Column Metadata 中指定 collation(见 Specifying collations in the table schema 一节),否则默认使用二进制 collation。 - Column Metadata 表新增一行:
| Field Name | Description | |-|-| |__COLLATIONS| 存储在该字段中、或存储在该字段内且不含嵌套 struct 的 map/array 组合中的字符串的 collations。详见 Specifying collations in the table schema 一节 |
七、Collation 版本提示:collations的 Domain Metadata
RFC 规定,collations表特性的 Domain Metadata 中存放"客户端在写入时应为哪些版本的 collation 产生统计"的提示。其结构如下:
{ "writeVersions": { "ICU.en_US": ["72", "73"] } }含义解读:
writeVersions是一个 map,键是无版本的 collation 标识符(如ICU.en_US);- 值是建议产生统计的版本字符串列表(如
["72", "73"]),这些版本按写入端维护的版本集合给出; - 该结构帮助客户端在写入时无需逐个查看所有 AddFile 的统计即可选择合适的 collation 版本;
- 客户端允许忽略这些提示——它们是"hints"而非强约束。
结合 Writer 要求第 4、5 条:写入端在引入新版本统计时应把新版本加入writeVersions,在废弃某版本统计时(如 ICU 库升级)可将其从列表中移除。这也是为什么该特性必须依赖domainMetadata表特性——它需要一块可独立于表 schema 演化的元数据区域来维护版本清单。
八、Per-file Statistics:statsWithCollation结构
8.1 基本语义
Per-column statistics 记录文件中每一列的统计信息,其编码镜像实际数据的 schema。统计是可选的,并且允许在字段声明了非二进制 collation 时,仍提供 UTF-8 二进制统计。
RFC 给出了一个带 collation 字段的示例数据 schema:
|-- a: struct | |-- b: struct | | |-- c: long |-- d: struct |-- e: string collate ICU.en_US.72对应地,统计可以存储为如下 schema:
|-- stats: struct | |-- numRecords: long | |-- tightBounds: boolean | |-- minValues: struct | | |-- a: struct | | | |-- b: struct | | | | |-- c: long | |-- maxValues: struct | | |-- a: struct | | | |-- b: struct | | | | |-- c: long | |-- statsWithCollation: struct | | |-- ICU.en_US.72: struct | | | |-- minValues: struct | | | | |-- d: struct | | | | | | e: string | | | |-- maxValues: struct | | | | |-- d: struct | | | | | | e: string这个示例展示了三层设计:
- 顶层
minValues/maxValues继续服务于默认(二进制)比较场景——本示例中a.b.c是 long 类型,直接放在顶层; statsWithCollation是一个按"带版本的 collation 标识符"(如ICU.en_US.72)作 key 的结构,其内部再嵌套minValues/maxValues;- collated 字段(
d.e)的统计只出现在statsWithCollation.<version>之下,与顶层统计隔离,从而避免不同排序规则产生的 min/max 互相污染。
在 Spark 侧,统计 schema 的解析逻辑位于 DataSkippingReader.scala:getStatsColumnOpt接收"统计类型的路径"(对 collated 字符串而言其中包含版本化 collation 标识符)与"嵌套列名路径",两者都以逆序传入;方法沿 stats schema 逐层折叠查找字段,路径中任一环节不存在即返回None。这从实现上印证了 RFC 的结构设计:collated 统计是 stats schema 中真实存在、可被路径寻址的独立子树。
8.2 Per-column statistics 支持的类型
RFC 更新了 per-column statistics 的完整定义,下表按stats.tightBounds的取值区分语义:
| Name | Description(stats.tightBounds=true) | Description(stats.tightBounds=false) | |-|-|-| |nullCount| 该列的null值数量 | 若某列的nullCount等于物理记录数(stats.numRecords),则该列所有有效行都必须是null(反之不一定成立);若nullCount等于 0,则该列所有有效行都非null(反之不一定成立);若nullCount是除这两种特殊情况外的任意值,则不携带任何信息,应视同缺失 | |minValues| 一个等于该文件中此列最小有效值1的值;若所有有效行均为 null,则不携带信息 | 一个小于等于该文件中此列所有有效值1的值;若所有有效行均为 null,则不携带信息 | |maxValues| 一个等于该文件中此列最大有效值1的值;若所有有效行均为 null,则不携带信息 | 一个大于等于该文件中此列所有有效值1的值;若所有有效行均为 null,则不携带信息 | |statsWithCollation| 针对不使用二进制 collation 的字符串列的 minValues 与 maxValues | 与顶层 minValues/maxValues 语义相同,但把 minValues 与 maxValues 都包装进一个以生成它们的 collation 为 key 的对象中 |
tightBounds的语义区分值得单独强调:当tightBounds=true时,统计是"精确边界"(min 恰好等于最小有效值);当tightBounds=false时,统计退化为"不等式边界"(min ≤ 所有有效值,max ≥ 所有有效值),此时文件跳过的条件判断会相应放宽,但安全性不变。而statsWithCollation在两种模式下都遵循"同层语义、按 collation 分桶"的原则。
九、协议落地的整体视图与后续阅读
将本 RFC 的条款与仓库实现对照,可以形成一张完整的落地视图:
| RFC 条款 | 仓库实现佐证 | |-|-| | Writer Version 7 +collationswriter feature | TableFeatures.java 中Collations定义super("collations", 7),属 writer-only 特性,并保留collations-preview兼容旧名 | | 三段式标识符PROVIDER.NAME[.VERSION]| CollationIdentifier.java 的fromString/toString/equals实现 | | 字符串类型携带 collation,默认 SPARK UTF8_BINARY | StringType.java 中StringType.STRING默认 collation 为SPARK.UTF8_BINARY| | collated 统计以版本化标识符寻址 | DataSkippingReader.scala 中统计路径包含版本化 collation 标识符 |
如果希望继续深入,推荐按以下顺序阅读仓库相关材料:
- RFC 原文:protocol_rfcs/collated-string-type.md;
- 通用表特性机制与 writerFeatures 的完整列表:TableFeatures.java;
- collation 标识符单元测试(覆盖
fromString解析、大小写归一化、版本可选性等边界):CollationIdentifierSuite.scala; - 字符串类型与 collation 的关联测试:StringTypeTest.java;
- 数据跳过(含 collated 统计路径)的测试:DataSkippingUtilsSuite.scala 与 StatsSchemaHelperSuite.scala。
十、总结
Collated String Type 协议以三个精心设计的机制解决了"带排序规则的字符串比较"与"基于统计的文件跳过"之间的正确性矛盾:
- schema 中的
__COLLATIONS元数据声明列的默认排序规则(无版本),读者据此决定比较与排序行为,而不支持该特性的客户端仍可用 UTF-8 二进制规则安全读取; statsWithCollation按版本化 collation 分桶存储 min/max,使不同排序规则、不同 ICU 版本的统计互不干扰,且允许写入端在声明了非二进制 collation 时仍然只写二进制统计;domainMetadata中的writeVersions提示让写入端不必扫描全部文件即可选择合适的统计版本,并在引擎升级(如 ICU 升级)时平滑迁移版本集合。
整个设计始终坚持一个原则:统计只能被与其完全一致(含版本)的 collation 复用——这是文件跳过正确性的最后一道防线,也是本 RFC 最值得所有引擎实现者牢记的一条约束。
字符串列在固定前缀长度处截断;时间戳列截断到毫秒。
↩ ↩ ↩ ↩
【免费下载链接】deltaAn open-source storage framework that enables building a Lakehouse architecture with compute engines including Spark, PrestoDB, Flink, Trino, and Hive and APIs项目地址: https://gitcode.com/GitHub_Trending/del/delta
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考