Delta Lake 的 Z-Order 索引原理:为什么它能让查询性能提升数倍

深入讲解 Delta Lake Z-Order 索引原理,从数据跳过和文件裁剪机制出发,结合分区与 Liquid Clustering 的对比,说明 Z-ORDER BY 的真正价值、局限性以及落地时的注意事项。

从一个常见场景说起

很多团队把业务数据放进 Delta Lake 之后,会遇到一个非常熟悉的问题:表已经有日期分区,查询条件也带了日期,但加上一个 user_id 过滤之后,Spark 扫描的文件数量还是高得吓人。查看执行计划,发现大部分文件在数据跳过(Data Skipping)阶段就被判断为“可能存在数据”,因为每个文件的 user_id 最小值和最大值几乎覆盖了全表范围。

AI technology illustration

于是有人建议对 user_id 做 Z-Order。网上关于 Z-Order 的讨论很多,观点却相当分裂,有人说查询快了几倍,有人说完全没效果。这种差异多半不是因为 Z-Order 本身,而是因为对它的工作原理和适用边界理解不一样。这篇文章想把 Z-Order 从底层拆开,看看它到底做了什么,以及为什么对某些查询有效、对另一些查询无效。

数据跳过:Z-Order 发挥作用的基础

Delta Lake 并不像传统数据库那样维护一个独立的索引结构。它存储的是一张表的 Parquet 数据文件加上事务日志,事务日志里记录了每个文件的信息,包括各列的最小值、最大值、null 数量等统计信息。查询引擎在执行时,会把这些统计信息与查询条件做对比,判断某个文件是否完全不可能命中,如果是就不打开这个文件。这就是 Delta Lake 的 Data Skipping,也是它做文件裁剪的主要手段。

所以,想让查询扫描的文件更少,本质上不是“建一个索引”,而是让每个数据文件的统计边界都尽量窄。理想情况下,每个文件在过滤列上的区间都很小,且文件之间的区间互相不重叠,这样查询条件一进来,就能直接砍掉绝大多数文件。真实工程里虽然没有这么完美,但通过改变数据分布可以大幅逼近这个状态。

Z-Order 曲线:把多维空间压缩成一维

Z-Order 的核心是一种空间填充曲线。它可以按照一定规则,把多维空间中的点映射到一维有序排列,并且尽可能让原来在多维空间里靠近的点,在一维序列中也靠在一起。实现方式并不复杂:把各维度的整数表示做比特位交错排列。比如二维数据,先取两个数的二进制第 0 位,组成新的第 0、1 位,再取第 1 位,组成第 2、3 位,依次类推。

def zorder2D(x, y):
    result = 0
    for i in range(32):
        result |= ((x >> i) & 1) << (2 * i)
        result |= ((y >> i) & 1) << (2 * i + 1)
    return result

这个映射不是严格保序的,比如两个点在一维上距离近,不代表它们在二维空间里一定近。但在统计意义上,Z-Order 能让聚类效果远好于随机分布。对数据湖这种以文件为单位做裁剪的场景来说,这个“大概其”的局部性已经足够有价值。

在 Delta Lake 里使用 Z-Order,一般不是直接调用这个函数,而是执行 OPTIMIZE 语句并指定 ZORDER BY 列:

OPTIMIZE events
ZORDER BY (user_id, ts);

执行之后,Spark 会读取原始表,按照 Z-Order 值进行全局重排,然后按排序产生的自然区间生成新的数据文件。你不需要手动控制每个文件里包含哪些值,Spark 会结合目标文件大小和聚类密度来切分。

为什么 Z-Order 能减少数据文件扫描

还是回到 user_id 的例子。假设全表有 100 亿条记录,user_id 从 1 到 1 亿。如果没有 Z-Order,user_id 记录大概率随机散落在每个文件中,因此每个文件的 user_id min/max 区间几乎都是全表的 1 到 1 亿。这时候 WHERE user_id = 9527,查询引擎查看每个文件的统计信息,发现 9527 都落在区间内,于是所有文件都必须打开。实际上你只查一个用户,却扫描了全表。

执行过 ZORDER BY (user_id, ts) 之后,相同或相近 user_id 的记录被集中到一批文件里。一个文件可能只覆盖用户 ID 在 9520 到 9580 之间的数据,甚至更窄。再执行同样的查询时,查询引擎只需要检查这些区间的文件,从几百个文件变成两三个文件,扫描量自然就降下来了。这也是 Z-Order 能让查询性能提升数倍的根本原因。

需要强调,Z-Order 不是传统意义上的索引,它不产生一个独立的映射结构,也不像 B+Tree 那样能直接定位到行。它的作用是让文件统计信息变得更紧凑,从而提高 Data Skipping 的命中率。因此,Z-Order 只对带有该列过滤条件的查询有效,对全表扫描没有任何帮助。

Z-Order 能替代分区吗

很多人把它和分区放在一起比较,其实两者不在一个维度。分区是目录级的粗粒度裁剪,通过目录名直接排除整批数据。Z-Order 是文件级的细粒度布局优化,作用于同一分区或整个表内的文件。分区列如果选错,会造成大量小文件和写入倾斜;而 Z-Order 不会改变目录结构,只是重写文件,风险相对小很多。

Delta Lake 3.0 之后出现的 Liquid Clustering,则试图把 Z-Order 的收益变成一种自动维护的机制,它允许在写入时动态聚类,不需要定期跑 OPTIMIZE。三者的区别可以用下面这张表来概括。

维度 分区 Z-Order Liquid Clustering
裁剪粒度 目录级 文件级 文件级(增量)
更适合的列类型 低基数、范围过滤 中/高基数、等值过滤 两者兼顾,但需版本支持
写入负担 写路径固化目录 OPTIMIZE 时全量重写 写入时自动维护,成本较低
维护成本 基本无需维护 需要定期 OPTIMIZE 自动,但需要更新到新版本

使用 Z-Order 前需要知道的三个误区

误区一:Z-Order 是索引,能精准定位数据

这是最常见的理解偏差。Delta Lake 没有单独的索引文件,Z-Order 也不会产生类似 Dict 二级索引的结构。它只是改变了数据文件的物理分布,让查询引擎基于文件统计信息可以做更高效的数据跳过。查询还是要扫文件,但扫的文件数量变少了很多。

误区二:Z-ORDER BY 列越多越好

Z-Order 在多维空间中的局部性保留能力会随着维度增加而下降,而且每个文件的 min/max 统计范围也会因为维度增多而变宽,查询裁剪效果随之减弱。实际工程里一般建议控制在 2 到 4 列。如果你把五六个列都塞进 ZORDER BY,排序成本上去了,聚类质量却下来了,最后往往看不到明显收益。

误区三:执行一次 OPTIMIZE 就能一劳永逸

Z-Order 是对历史文件做重排,后续新写入的文件并不天然继承这种排序。随着每次 append 产生的新文件增加,数据又会慢慢恢复成随机分布状态。要保持稳定性能,最好把 OPTIMIZE ZORDER BY 加到周期运维里,比如每天或每周对热分区执行一次,而不是只在建表时抱佛脚。

落地建议

在决定要不要用 Z-Order 之前,先找一下真正慢的查询,尽量用 EXPLAIN 看消耗的 scan files 数量。如果文件数量本身就很少,比如只有几十个,Z-Order 带来的性能提升会非常有限,没必要折腾。

选择 Z-ORDER BY 列时,优先考虑高频等值过滤条件中的业务键。比如用户分析场景里的 user_id,订单场景里的 order_id,推荐实验场景里的 device_id。配合时间范围条件做第二列,基本就能覆盖最常见的查询模式。不要选择 status 这种取值只有几个的列,这种列的统计区间本来就窄,Z-Order 加不了什么分。

  • 一次 Z-ORDER BY 不要超过 4 列,通常 2 到 3 列最稳。
  • 尽量对热分区执行 OPTIMIZE WHERE,降低重写成本和历史数据被反复 rewrite 的代价。
  • 和文件大小平衡一起考虑。如果文件太小,先 compaction 再 Z-Order,否则收益会被文件数淹没。
  • 监控 OPTIMIZE 后的查询扫描文件数,判断数据跳过是否变得更高效。

最后要有一个预期管理:Z-Order 并不是为了全表扫描这类查询服务的。真正能从数倍提升中受益的,是那些本来就被过滤条件限制得非常窄、但在文件层面无法快速裁剪的查询。换句话说,Z-Order 是给数据跳过机制“喂料”,让统计信息变得更犀利,而不是替代查询引擎本身的执行能力。

小结

Delta Lake 的 Z-Order 看似神秘,本质上仍是数据布局优化的一个工具。它的原理建立在文件级统计和数据跳过机制之上,而不是建立一个新的索引系统。理解了这一点,你就能解释为什么不同团队对 Z-Order 的评价差异巨大:只要过滤条件和 Z-ORDER BY 列匹配,且文件规模足够大,性能提升确实可以是数量级的;反过来,如果列选错、文件太碎,或者没有定期维护,它很可能沦为一次昂贵的表重写。

希望这篇文章能把 Z-Order 的适用条件讲清楚。在实际使用时,建议先从小范围验证开始,用执行计划里的扫描文件数量和数据跳过指标说话,而不是怀着一颗盲目调优的心把 OPTIMIZE ZORDER BY 当成万能药。

原创文章,作者:fudengji,如若转载,请注明出处:https://fudengji.cn/article/940/

(0)
上一篇 2小时前
下一篇 1小时前

相关推荐