Clickhouse技术分享: 分区裁剪
本文代码截图, 基于2021-07-19的master版本
DataPart存储
1 | CREATE TABLE default.lineorder_local |
选了Clickhouse社区官方的SSB测试套的lineorder
表为例, 他以toYear(LO_ORDERDATE)
为分区键, 此时插入一些数据, 在Clickhouse DataPart级别的目录下, 会出现以下文件
其中跟分区相关的有两个: partition.dat
和minmax_LO_ORDERDATE.idx
partition.dat
存储的是Partition的具体数值, 对应Clickhouse的源码
1 | struct MergeTreePartition |
就是我们在system.parts
表中看到的partition
数值, 其中value
的类型是Row
, 对应表达式计算后的结果.
其中load
函数是读取dat
文件的数值到row
中, store
是存储row
数据写入到文件里.
minmax_LO_ORDERDATE.idx
存储的是对应字段LO_ORDERDATE
的最大值和最小值范围
注意是
LO_ORDERDATE
的值, 而非toYear(LO_ORDERDATE)
的值
对的代码在IMergeTreeDataPart
的内部struct
1 | struct MinMaxIndex |
与partition.dat
类似, 他们都有load
和store
方法, 但是MinMaxIndex
额外有update
和merge
, 因为DDL的SQL并不会改变partition
, 但一个Delete where
的语句有可能改变了上下界.
举例总结
举个例子来说, 如果一组数据, 只有3个值, 其中 LO_ORDERDATE
列为: 1992-01-01
,1992-06-02
和 1992-12-01
, 那么partition.dat
存储的值就是toYear(LO_ORDERDATE)
的数值, 也就是1992
; 而minmax_LO_ORDERDATE.idx
存储的是1992-01-01
和1992-12-01
, 表示这个区间的上下确界.
数据查询
单调函数
下图是一个比较典型的单调递增函数:
下图是一个典型的非单调递增函数
单调函数的特点, x的最大最小值, 必然是y的最大最小值(单调递增).
所以很容易想到, 如果Clickhouse某个函数是单调的, 那么能通过DataPart上的minmax索引, 来过滤整个DataPart.
CK的整个分区裁剪实际上是在分区column上的minmax索引, 而非字面理解的分区裁剪
Clickhouse函数
在Clickhouse的IFunction.h
文件里面, 有两个关于单调性的CK函数接口: hasInformationAboutMonotonicity
和getMonotonicityForRange
其中实现这些函数的, 基本上都可以实现分区裁剪功能, 没实现的不能实现分区裁剪.
Clickhouse Function接口在21年经过重构, 有一部分函数实现新的接口, 一部分实现老接口
实现IFunctionBase的函数
实现IFunction的函数
DataPart筛选
MergeTree
引擎的数据筛选的代码封装在MergeTreeDataSelectExecutor
类中
首先, 根据分区键和查询语句, 构造KeyCondition
然后, 遍历每个DataPart过滤不符合条件的, 保留能够命中minmax
索引的DP
在方法checkInHyperrectangle
判断函数链是不是都是单调, 例如toYear(toDate('2010-10-10'))
这类两个函数复合的表达式, 两个都是单调函数, 因此单调链是有效的.
限制
上文提到实现hasInformationAboutMonotonicity
基本上能够分区裁剪, 之所以说基本上, 原因在于还有一些例外.
回到上面的KeyCondition
, 在整个构造过程中, 有一个非常重要的函数isKeyPossiblyWrappedByMonotonicFunctionsImpl
来判断是否单调.
从代码上看到, 只有参数个数是1或者2个才能命中; 有2个参数的, 其中一个需要是常量表达式.
因此类似date_trunc(unit, value[, timezone])
这函数也是单调的, 但是因为有3个参数, 因此也是无法命中索引.
这个是代码实现问题, 只是3个参数的函数非常少, 没啥动力去实现.