跳到主要内容
跳到主要内容

表分区

ClickHouse中的表分区是什么?


分区将数据部分组织到MergeTree引擎系列中的表中,形成有序的逻辑单元,这是一种以概念上有意义的方式组织数据的方法,并与特定标准(如时间范围、类别或其他关键属性)对齐。这些逻辑单元使数据更容易管理、查询和优化。

分区依据

在通过PARTITION BY子句定义表时,可以启用分区。此子句可以包含任何列上的SQL表达式,其结果将定义某一行属于哪个分区。

为此,我们通过添加一个 PARTITION BY toStartOfMonth(date)子句来增强 表部分是什么的示例表,该子句根据物业销售的月份组织表的数据部分:

您可以在我们的ClickHouse SQL Playground中查询此表

磁盘上的结构

每当一组行被插入到表中时,ClickHouse不会创建(至少)一个包含所有插入行的单一数据部分(如这里所述),而是为每个插入行中唯一的分区键值创建一个新的数据部分:


ClickHouse服务器首先根据示例插入中4行的分区键值 toStartOfMonth(date)来拆分行。 然后,对于每个识别出的分区,这些行按常规进行处理,执行多个顺序步骤(① 排序,② 拆分为列,③ 压缩,④ 写入磁盘)。

请注意,启用分区后,ClickHouse会自动为每个数据部分创建MinMax索引。这些索引只是用于分区键表达式中的每个表列的文件,包含该列在数据部分中的最小值和最大值。

每个分区合并

启用分区后,ClickHouse仅会合并分区内的数据部分,而不会跨分区合并。我们为上面的示例表简单绘制如下:


如上图所示,属于不同分区的部分永远不会被合并。如果选择一个高基数的分区键,则分布在数千个分区上的部分将永远不是合并候选,超出预配置的限制,导致可怕的 Too many parts 错误。解决这个问题的方法很简单:选择一个基数在1000到10000之间的合理分区键。

监控分区

您可以使用虚拟列 _partition_value查询我们示例表的所有现有唯一分区列表:

另外,ClickHouse在system.parts系统表中跟踪所有表的所有部分和分区,以下查询返回关于我们上面的示例表的所有分区列表,以及每个分区中的当前活动部分和行总数:

表分区的用途是什么?

数据管理

在ClickHouse中,分区主要是一项数据管理功能。通过基于分区表达式逻辑组织数据,每个分区可以独立管理。例如,上述示例表中的分区方案使得场景得以实现,只有过去12个月的数据会保留在主表中,利用TTL规则自动删除旧数据(请参见DDL语句中添加的最后一行):

由于该表由 toStartOfMonth(date)分区,满足TTL条件的整个分区(表部分的集合)将被删除,从而使清理操作更高效,而不必重写部分

同样,不是删除旧数据,而是可以将其自动高效地移动到更具成本效益的存储层中:

查询优化

分区可以帮助提高查询性能,但这在很大程度上依赖于访问模式。如果查询仅针对少数几个分区(理想情况下是一个),性能可能会有所改善。这通常只有在分区键不在主键中,并且您正在按其过滤时才有用,如下面示例查询所示。

该查询在上述示例表上运行并计算出2020年12月在伦敦所有已售物业的最高价格,按照同时使用表的分区键中的一个列(date)和主键中的一个列(town)进行过滤(date并不是主键的一部分)。

ClickHouse通过应用一系列剪枝技术来处理该查询,以避免评估不相关的数据:


分区剪枝MinMax索引用于忽略整个不符合查询对表分区键中使用的列的过滤条件的分区(部分集合)。

粒度剪枝:对于步骤①之后的剩余数据部分,使用其主索引忽略所有逻辑上与查询对表主键中使用的列的过滤条件不匹配的粒度(行块)。

我们可以通过检查我们上面示例查询的物理查询执行计划,通过EXPLAIN子句:

上面的输出显示:

① 分区剪枝:EXPLAIN输出的第7到18行显示,ClickHouse首先使用date字段的MinMax索引来识别3257个现有粒度(行块)中的11个,它们存储在436个现有活动数据部分中的1个中,这些数据部分包含匹配查询的date过滤条件的行。

② 粒度剪枝:EXPLAIN输出的第19到24行表明,ClickHouse随后使用步骤①中标识的数据部分的主索引(在town字段上创建)进一步将粒度数量(可能也包含匹配查询的town过滤条件的行)从11减少到1。这在我们进一步上面的查询运行中打印的ClickHouse客户端输出中也得到了反映:

这意味着ClickHouse在6毫秒内扫描和处理了1个粒度(8192行块),以计算查询结果。

分区主要是数据管理功能

请注意,在所有分区上查询通常比在未分区表上运行相同的查询会更慢。

有了分区,数据通常分布在更多的数据部分上,这通常导致ClickHouse扫描和处理大量数据。

我们可以通过在表部分是什么示例表(未启用分区)和我们上面的当前示例表(启用分区)上运行相同查询来证明这一点。这两个表包含相同的数据和行数:

然而,启用了分区的表具有更多的活动data parts,因为,如上所述,ClickHouse仅在分区内合并数据部分,而不跨分区合并:

如上所示,分区表 uk_price_paid_simple_partitioned 拥有超过600个分区,因此有306个活动数据部分。而对于我们的未分区表 uk_price_paid_simple,所有初始数据部分都可以通过后台合并合并为单个活动部分。

当我们检查上面示例查询的物理查询执行计划时,使用EXPLAIN子句,针对分区表的查询未使用分区过滤,下面输出的第19和20行中可以看到,ClickHouse识别出671个现有粒度(行块),分布在436个现有活动数据部分上,这些部分可能包含匹配查询过滤的行,因此将被查询引擎扫描和处理:

对于相同的示例查询在未分区表上运行的物理查询执行计划显示,输出的第11和12行中可以看到,ClickHouse识别出241个现有行块,在表的单个活动数据部分中,这些行块可能包含匹配查询过滤的行:

对于运行分区版本表的查询,ClickHouse在90毫秒内扫描和处理了671个行块(约550万行):

而对于运行未分区表的查询,ClickHouse在12毫秒内扫描和处理了241个行块(约200万行):