相关文章推荐

在上一篇介绍 Delta Lake原理 的文章中,我提到Delta的商业版有很多开源版没有的“黑科技”,其中有一项就是 Z-order 。Z-order到底是什么,有什么“神奇功效“, 使得Databricks藏着掖着不想拿出来开源呢? 这篇文章就来讲一讲这个话题。

首先需要明白Z-order是什么,以及能够帮助我们解决什么问题。摘抄下Wikipedia对Z-order的 定义

In mathematical analysis and computer science, functions which are Z-order, Lebesgue curve, Morton space filling curve,[1] Morton order or Morton code map multidimensional data to one dimension while preserving locality of the data points.

这段定义很抽象,还用了很多数学名词,像是勒贝格曲线(Legesgue curve),莫顿空间填充曲线(Morton space filling curve)等,很不好理解。但其实真正重要的是最后一句

Z-order可以将多维数据映射到一维,同时保持数据点的局部性

Delta正是借助Z-order保持数据局部性的优势,使得读取数据时可以skip掉大量block,从而极大地提升了Delta的性能。性能提高多少呢?引用下Databricks官方博客对z-order的介绍

Using Databricks Delta’s built-in data skipping and ZORDER clustering features, large cloud data lakes can be queried in a matter of seconds by skipping files not relevant to the query. In a real-world cybersecurity analysis use case, 93.2% of the records in a 504 terabytes dataset were skipped for a typical query, reducing query times by up to two orders of magnitude.

In other words, Databricks Delta can speed up your queries by as much as 100X .

在Databricks的一个真实场景中,Z-order帮助过滤掉了504TB数据中的93.2%,共计470TB数据。换言之, 提高了查询性能达100倍之多

Z-order的一种直观解释

按照我们的惯例,对于任何一项技术我们首先要问的一个问题就是: 这项技术为了解决什么问题? 不过这次我不直接给出答案,而是先用一个例子来引出这个问题。

《理解ClickHouse的MergeTree引擎》 这篇文章中,我们提到了ClickHouse里的数据是 按照主键排序存储 的,因此通过主键可以快速地过滤掉无关的数据。

例如对于下面这张表

x y z
1 8192 0
2 8191 1
... ... ...
81920 1 81919

其中x是递增的整数,y是8192到1循环的整数,z还是递增的整数。共计81920条数据。

ClickHouse会将数据分成10个granule(每8192条数据为一个granule),分别存储为10个文件,并在索引文件里保存每个granule的主键最大值和最小值。假设主键列为x,则索引如下表所示。

granule min max
1 1 8192
2 8193 16384
... ... ...
10 73729 81920

此时如果执行下面这条查询语句

SELECT sum(z) FROM test WHERE x < 10000

ClickHouse会根据索引确定只需要读取1和2这两个granule,从而减少了80%的数据读取量。

可是如果组成主键的列不止一列呢?也不复杂,只要把多列组成tuple,并且按照每列的大小关系定义tuple的大小关系即可。例如主键是x和y组成的复合主键,则索引文件的内容如下

granuleminmax
11,81928192,1
28193,819216384,1
.........
1073729,819281920,1

对于过滤条件包括x和y两列的查询

SELECT sum(z) FROM test WHERE x < 10000 AND y > 10000

或是只包括x的查询

SELECT sum(z) FROM test WHERE x > 70000

都可以继续使用索引文件来快速地过滤掉不满足条件的granule(这里我就不具体演示了)。

然而如果是只包括y的查询

SELECT sum(z) FROM test WHERE y > 70000

索引就完全无法使用了。 这是因为数据是按照(x, y)的值排序的,本质上来说首先x列是有序的,只有在x值相同时,y列才保证有序。所以这种基于x列排序的索引必须在过滤条件中包含x列时,才能充分利用有序的特性来过滤数据。

那么有没有一种索引,或者说一种排序方式,使得数据可以在x列和y列上“平等地”有序呢? 这样无论x列还是y列,都可以单独地作为过滤条件。

而这正是Z-order希望解决的问题。

Z-order就是这样一种在复数列上“平等的”排序方式。

Z-order的基本思想

上一节提到Z-order本质是一种“排序方式”,使得数据按照这种顺序存储后,无论对主键的哪一列,都表现出一定的“有序性”,从而在查询时可以过滤掉许多无关的数据。

那么如何做到在复数列上有序呢?其实这是做不到的,Z-order也没有做到这一点。所以实际上Z-order退而求其次,实现的是在复数列上的“局部性”

什么是局部性呢?直观地来讲,就是数值接近的值不会相距太远。例如对于(3, 8)这个值,如果按照Z-order进行排序,则(4, 8)(3, 9)或是(2, 7),都不会距离(3, 8)太远。

具体来说,Z-order其实就是下图所示的这种“曲折型”的排序方式

图中的每个圆点代表(0, 0)(2, 3)这样的数据点。可以看到,在Z-order的顺序里,(0, 0)(1, 0)前,(1, 0)(0, 1)前,然后(1, 1)又在(2, 0)前,以此类推。因为整个图看起来像是在反复地画“Z”字,所以这种排序方式被称为Z-order。

至于如何计算出这种排序,我先按下不表,会放到最后一节中详细讲解。

以下举一个具体例子来展示下Z-order如何应用于存储领域。假设我们有如下的数据,以x和y作为组成Z-order的两列,z列为需要做聚合的指标列。

xyz
001
031
101
121
211
231
301
331

按照Z-order排序以后,数据会变成下面这样(建议大家可以对照着上面的图,手动地对这些数据进行一下排序)。

(0, 0, 1)
(1, 0, 1)
(3, 0, 1)
(2, 1, 1)
(1, 2, 1)
(0, 3, 1)
(2, 3, 1)
(3, 3, 1)

接下来就只需要把数据按顺序保存成文件即可。至于这种存储的顺序有何好处?下一节会来谈谈。

Z-order和Skip Index

上一节展示了Z-order是如何对数据进行排序的,接下来谈谈这种存储顺序如何影响查询性能。其实Z-order最主要的效果是提高了数据在多个维度的局部性,从而提高Skip Index的效率。 注意只有在多个维度上构建索引才需要用到Z-order,因为如果只有一个维度,那直接对那个维度排序的局部性就是最好的。

在上周的文章《ClickHouse的索引原理》中,我讲了ClickHouse的Skip Index的原理。以其中的minmax索引为例,来看看Z-order对提高minmax索引的效率有哪些帮助。

还是以上一节的例子为例,假设设置ClickHouse的granularity为2(即每2行为一个granule)。如果按照ClickHouse默认的主键排序方式,数据的存储顺序如下

(0, 0, 1)
(0, 3, 1)
(1, 0, 1)
(1, 2, 1)
(2, 1, 1)
(2, 3, 1)
(3, 0, 1)
(3, 3, 1)

按照每2行为一个granule,最后产生的minmax索引就是

granulex_minx_maxy_miny_max
00003
11102
22213
33303

从minmax索引可以很明显地看出,由于数据以(x, y)的顺序存储,所以在x列有很好的局部性,每个granule的(x_min, x_max)都是不相交的。而y列的局部性就比较差了,有比较多的重合。

这时如果我们运行下面这个简单的“过滤-聚合”查询

SELECT sum(z) FROM test WHERE y = 2

因为过滤条件是y=2,通过minmax可知需要读取的granule是0, 1, 2, 3,共4个。索引完全没有起到过滤的效果。

再来看看假如以Z-order对数据进行排序情况会怎样。上一节展示过数据以Z-order排序的结果如下

(0, 0, 1)
(1, 0, 1)
(3, 0, 1)
(2, 1, 1)
(1, 2, 1)
(0, 3, 1)
(2, 3, 1)
(3, 3, 1)

minmax索引则变成

granulex_minx_maxy_miny_max
00100
12301
20123
32333

运行和上文同样的查询sql。

SELECT sum(z) FROM test WHERE y = 2

在新的minmax索引的帮助下,确定需要查询的granule只有2这一个。索引的过滤效率是75%。

于是我们看到,相比主键排序的存储方式,数据经过Z-order排序后,索引的过滤结果数从4个减少到了1个,效率提升了4倍之多

实现Z-order

在讲完了Z-order的基本思想以及它能带来的好处之后,剩下的就只剩“如何实现”这个大话题了。这一块我可能不会展开太多,毕竟大部分读者都不太会需要自己去实现Z-order。

关于实现我只讲一个主题,那就是如何对一个数据集使用Z-order进行排序

上文中我们展示了Z-order是一种Z字型的排序方式,那么对于任意一个数据集如何应用这个排序呢?换言之,怎么知道哪个数据在前,哪个数据在后?

我们先从最简单的一种情况开始,那就是Z-order只包含两列(x, y)(以下简称坐标),而且x和y都是整数类型

实现上,我们使用一个值来确定坐标在Z-order上的顺序,这个值就被称为Z-Address。对每个坐标我们都可以计算出一个对应的Z-Address,而坐标在Z-order上的前后关系,就是Z-Address的大小关系。

至于Z-Address如何计算,办法是将x和y表示为二进制以后,每个bit位相互插值即可产生Z-Address。

对于更多列的,以及不是整数类型的情况,相对就更加复杂了,我这里不再细说。简单地来说,就是通过保序映射(order-preserving map)将其映射到整数以后,再使用整数的计算方法。

欢迎关注我的微信公众号:Data Dive,一个专注于大数据顶层认知底层原理的公众号。

  •  
    推荐文章