自定义分区键
在大多数情况下,无需使用分区键;在其他大多数情况下,除非是针对按天分区较为常见的可观测性场景,否则也不需要比“按月”更细粒度的分区键。
切勿使用过于细粒度的分区。不要按客户端标识符或名称对数据进行分区,而应将客户端标识符或名称设置为 ORDER BY 表达式中的第一列。
MergeTree 系列表支持分区,包括复制表和物化视图。
分区是按指定条件在表中对记录进行的逻辑归组。可以按任意条件设置分区,例如按月、按日或按事件类型。每个分区单独存储,以简化对这部分数据的操作。访问数据时,ClickHouse 会尽可能只访问最小数量的分区。对于包含分区键的查询,分区可以提升性能,因为 ClickHouse 会在选择分区内的 part 和 granule 之前,先根据分区进行过滤。
在创建表时,通过 PARTITION BY expr 子句指定分区。分区键可以是基于表列的任意表达式。例如,要按月进行分区,可以使用表达式 toYYYYMM(date_column):
分区键也可以是表达式元组(类似于主键)。例如:
在本示例中,我们按当前周内发生的事件类型进行分区。
默认情况下,不支持使用浮点类型作为分区键。要使用它,请启用设置 allow_floating_point_partition_key。
当向表中插入新数据时,这些数据会作为一个单独的数据部分(part),按主键排序后存储。在插入后的 10–15 分钟内,同一分区中的各个部分会被合并为一个完整的数据部分。
合并仅适用于分区表达式取值相同的数据部分。这意味着不应创建过于细粒度的分区(超过大约一千个分区)。否则,由于文件系统中文件数量和打开的文件描述符数量过多,SELECT 查询的性能会很差。
使用 system.parts 表可以查看表的数据部分和分区。例如,假设我们有一个按月分区的 visits 表。让我们对 system.parts 表执行 SELECT 查询:
partition 列包含分区的名称。在此示例中有两个分区:201901 和 201902。可以使用该列的值在 ALTER ... PARTITION 查询中指定分区名称。
name 列包含分区数据 part 的名称。你可以在 ALTER ATTACH PART 查询中使用该列来指定 part 的名称。
下面我们来拆解这个 part 名称:201901_1_9_2_11:
201901是分区名。1是数据块的最小编号。9是数据块的最大编号。2是 chunk 的层级(其在 MergeTree 中形成时所处的深度)。11是 mutation 版本(如果该 part 发生过 mutation)。
旧类型的表的 part 名称格式为:20190117_20190123_2_2_0(最小日期 - 最大日期 - 最小块编号 - 最大块编号 - 层级)。
active 列表示 part 的状态。1 表示活跃(active);0 表示非活跃(inactive)。例如,合并为更大 part 后保留下来的源 part 是非活跃的 part。损坏的数据 part 也会被标记为非活跃。
如示例所示,同一分区可以包含多个相互独立的 part(例如 201901_1_3_1 和 201901_1_9_2)。这表示这些 part 尚未被合并。ClickHouse 会周期性地合并已插入的数据 part,大约会在插入后 15 分钟触发一次合并。此外,你可以使用 OPTIMIZE 查询执行一次非计划的合并。示例:
非活跃的 parts 将在合并后大约 10 分钟内被删除。
查看一组 parts 和 partitions 的另一种方法,是进入该表所在的目录:/var/lib/clickhouse/data/<database>/<table>/。例如:
文件夹 '201901_1_1_0'、'201901_1_7_1' 等,是各个 part 的目录。每个 part 属于一个分区,并且只包含某一个月的数据(本示例中的表是按月分区的)。
detached 目录包含通过 DETACH 查询从表中分离出去的 part。损坏的 part 也会被移动到该目录,而不是直接删除。服务器不会使用 detached 目录中的这些 part。你可以在任何时候在该目录中添加、删除或修改数据——在你执行 ATTACH 查询之前,服务器都不会察觉到这些更改。
请注意,在正在运行的服务器上,你不能在文件系统中手动更改 part 的集合或其数据,因为服务器不会感知到这些变更。对于非复制表,你可以在服务器停止时进行此操作,但不推荐这样做。对于复制表,在任何情况下都不能更改 part 的集合。
ClickHouse 允许你对分区执行操作:删除分区、在表之间复制分区,或者创建备份。所有操作的完整列表请参见 Manipulations With Partitions and Parts 一节。
使用分区键进行 GROUP BY 优化
对于某些表的分区键与查询的 GROUP BY 键的特定组合,可以针对每个分区独立执行聚合。 这样在最后我们就不必合并所有执行线程产生的部分聚合结果, 因为可以保证相同的 GROUP BY 键值不会同时出现在两个不同线程的工作集里。
一个典型示例如下:
此类查询的性能在很大程度上取决于表结构。因此,该优化默认未启用。
获得良好性能的关键因素:
- 查询涉及的分区数量应足够大(大于
max_threads / 2),否则查询将无法充分利用机器资源 - 分区不应过小,否则批处理会退化为逐行处理
- 分区大小应大致相当,这样所有线程执行的工作量大致相同
建议对 partition by 子句中的列应用某种哈希函数,以便在分区之间均匀分布数据。
相关设置如下:
allow_aggregate_partitions_independently- 控制是否启用该优化force_aggregate_partitions_independently- 当从正确性角度看可用,但因内部评估其性价比的逻辑而被禁用时,强制使用该优化max_number_of_partitions_for_independent_aggregation- 对表可包含的最大分区数量的硬性限制