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

表分区

ClickHouse 中的表分区是什么?


分区将表中 数据片段 组织成有序、逻辑单元,这属于 MergeTree 引擎系列 的一种数据组织方式,这种组织方式在概念上是有意义的,并且与特定的标准(如时间范围、类别或其他关键属性)保持一致。这些逻辑单元使得管理、查询和优化数据变得更加容易。

按分区

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

为了说明这一点,我们通过添加 PARTITION BY toStartOfMonth(date) 子句来 增强 什么是表片段 示例表,这样将表的数据片段按属性销售的月份进行组织:

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

磁盘上的结构

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

插入处理

ClickHouse 服务器首先将示例插入中四行的数据按其分区键值 toStartOfMonth(date) 进行拆分。然后,对于每个识别的分区,行按照 正常的方式 进行处理,包括多个顺序步骤 (① 排序,② 列拆分,③ 压缩,④ 写入磁盘)。

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

每个分区的合并

在启用分区的情况下,ClickHouse 只 合并 内部的数据片段,而不是跨分区。我们为上述示例表做一个简单的示意:

分区合并

如上图所示,属于不同分区的片段永远不会被合并。如果选择了高基数的分区键,那么分布在数千个分区上的片段将永远不会成为合并候选者——超过预配置的限制并导致 dreaded Too many parts 错误。解决这个问题很简单:选择一个基数在 1000 到 10000 之间的合理的分区键 参考

监控分区

您可以通过使用虚拟列 查找 我们示例表中所有现有唯一分区的列表,使用虚拟列 /engines/table-engines#table_engines-virtual_columns _partition_value

另外,ClickHouse 在 system.parts 系统表中跟踪所有表的所有部分和分区,以下查询 返回 所有分区的列表,以及每个分区中活动部分的当前数量和这些部分中行的总数:

表分区的用途是什么?

数据管理

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

由于表是按 toStartOfMonth(date) 分区的,因此符合 TTL 条件的整个分区(表片段 的集合)将被删除,从而使清理操作更高效,而不必 重写片段

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

查询优化

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

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

ClickHouse 通过应用一系列修剪技术,

以避免评估不相关的数据来处理该查询:

分区修剪

分区修剪:使用 MinMax 索引 来忽略逻辑上无法与查询过滤器匹配的整个分区(片段集合)。

粒度修剪:在步骤 ① 之后,对剩余的数据片段使用其 主索引 来忽略逻辑上无法与查询的过滤器匹配的所有 粒度(行块)。

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

上述输出显示:

① 分区修剪:EXPLAIN 输出中第 7 行到第 18 行显示,ClickHouse 首先使用 date 字段的 MinMax 索引 来识别 1 个包含匹配查询 date 过滤器的行的 11 个现有 粒度(存储的行块)。

② 粒度修剪:EXPLAIN 输出中第 19 行到第 24 行表示,ClickHouse 然后使用在步骤 ① 中识别的数据部分的 主索引(创建在 town 字段上)来进一步减少粒度的数量(包含潜在也匹配查询 town 过滤器的行)从 11 减少到 1。 这也反映在我们为查询打印的 ClickHouse 客户端输出中:

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

分区主要是一种数据管理功能

请注意,跨所有分区的查询通常比在非分区表上运行相同的查询要慢。

使用分区后,数据通常分散在多个数据片段中,这通常会导致 ClickHouse 扫描和处理更多的数据量。

我们可以通过在没有分区的表上和在启用分区的表上运行相同查询来证明这一点。

两个表 包含 相同的数据和行数:

然而,启用分区的表 更多活动的 数据片段,因为,如前所述,ClickHouse 只 合并 内部的数据片段,而不是跨分区:

如上所述,分区表 uk_price_paid_simple_partitioned 有 306 个分区,因此至少有 306 个活动数据片段。而在我们的非分区表 uk_price_paid_simple 中,所有 初始 数据片段都可以通过后台合并合并为一个活动部分。

当我们 检查 启用分区情况下的物理查询执行计划,通过 EXPLAIN 子句对上述示例查询进行分析时,我们可以在输出的第 19 和 20 行看到 ClickHouse 确认了 671 个存在于 436 个活跃数据片段中的 粒度(行块),其中可能包含匹配查询的过滤器的行,并因此将被查询引擎扫描和处理:

相同示例查询的物理查询执行计划显示,当在无分区的表上运行时,在输出中的第 11 和 12 行中,ClickHouse 确认 241 个存在于表的单个活动数据部分中的行块,可能包含匹配查询过滤器的行:

启用分区的表上运行 查询时,ClickHouse 扫描和处理了 671 个行块(约 550 万行)耗时 90 毫秒:

而在 运行 无分区的表时,ClickHouse 扫描和处理了 241 个行块(约 200 万行)耗时 12 毫秒: