ClickHouse 中的表分区是什么?
分区将 数据块 在 MergeTree 引擎家族 中分组为有组织的逻辑单元,这是一种组织数据的方式,在概念上是有意义的,并且与特定标准(例如时间范围、类别或其他关键属性)对齐。这些逻辑单元使数据更易于管理、查询和优化。
PARTITION BY
可以在定义表时通过 PARTITION BY 子句 启用分区。该子句可以包含任何列上的 SQL 表达式,其结果将定义行属于哪个分区。
为了说明这一点,我们 增强 了 什么是表数据块 示例表,添加了一个 PARTITION BY toStartOfMonth(date) 子句,该子句根据房产销售的月份组织表的的数据块
CREATE TABLE uk.uk_price_paid_simple_partitioned
(
date Date,
town LowCardinality(String),
street LowCardinality(String),
price UInt32
)
ENGINE = MergeTree
ORDER BY (town, street)
PARTITION BY toStartOfMonth(date);
您可以在我们的 ClickHouse SQL Playground 中 查询 此表。
磁盘上的结构
每当将一组行插入到表中时,ClickHouse 不会创建(至少)一个包含所有插入行的单个数据块(如 此处 所述),而是为插入行中的每个唯一分区键值创建一个新的数据块。
ClickHouse 服务器首先根据分区键值 toStartOfMonth(date) 对示例插入中的 4 行进行分割。然后,对于每个识别出的分区,行将按照 通常 的方式进行处理,执行几个顺序步骤(① 排序,② 分割成列,③ 压缩,④ 写入磁盘)。
请注意,启用分区后,ClickHouse 会自动为每个数据块创建 MinMax 索引。这些只是每个用于分区键表达式的表列的文件,包含该列在该数据块内的最小值和最大值。
每个分区的合并
启用分区后,ClickHouse 仅 合并 分区内的,而不是跨分区的数据块。我们为上面的示例表绘制了示意图
如上图所示,属于不同分区的块绝不会合并。如果选择具有高基数的分区键,则跨数千个分区分散的块将永远不会成为合并候选对象 - 超过预配置的限制,并导致可怕的 Too many ^^parts^^ 错误。解决此问题很简单:选择具有 基数小于 1000..10000 的合理分区键。
监控分区
您可以使用 虚拟列 _partition_value 通过 查询 我们的示例表中所有现有唯一分区的列表。
SELECT DISTINCT _partition_value AS partition
FROM uk.uk_price_paid_simple_partitioned
ORDER BY partition ASC;
或者,ClickHouse 在 system.parts 系统表中跟踪所有表的所有部分和分区,并且以下查询 返回 我们的示例表上面的所有分区列表,以及当前活动部分的数量和这些部分中的行总数。
SELECT
partition,
count() AS parts,
sum(rows) AS rows
FROM system.parts
WHERE (database = 'uk') AND (`table` = 'uk_price_paid_simple_partitioned') AND active
GROUP BY partition
ORDER BY partition ASC;
表分区用于什么?
数据管理
在 ClickHouse 中,分区主要是一种数据管理功能。通过根据分区表达式以逻辑方式组织数据,可以独立管理每个分区。例如,上述示例表中的分区方案能够通过使用 TTL 规则(参见 DDL 语句中添加的最后一行)自动删除较旧的数据,从而仅在主表中保留最新的 12 个月的数据。
CREATE TABLE uk.uk_price_paid_simple_partitioned
(
date Date,
town LowCardinality(String),
street LowCardinality(String),
price UInt32
)
ENGINE = MergeTree
PARTITION BY toStartOfMonth(date)
ORDER BY (town, street)
TTL date + INTERVAL 12 MONTH DELETE;
由于表按 toStartOfMonth(date) 分区,满足 TTL 条件的整个分区(一组 表数据块)将被删除,从而使清理操作更有效,无需重写数据块。
类似地,旧数据可以自动有效地移动到更具成本效益的 存储层。
CREATE TABLE uk.uk_price_paid_simple_partitioned
(
date Date,
town LowCardinality(String),
street LowCardinality(String),
price UInt32
)
ENGINE = MergeTree
PARTITION BY toStartOfMonth(date)
ORDER BY (town, street)
TTL date + INTERVAL 12 MONTH TO VOLUME 'slow_but_cheap';
查询优化
分区可以帮助提高查询性能,但这在很大程度上取决于访问模式。如果查询仅针对几个分区(理想情况下是一个),则性能可能会提高。只有当分区键不在主键中并且您按其进行过滤时,这通常才有用,如以下示例查询所示。
SELECT MAX(price) AS highest_price
FROM uk.uk_price_paid_simple_partitioned
WHERE date >= '2020-12-01'
AND date <= '2020-12-31'
AND town = 'LONDON';
该查询在我们的示例表上运行,并 计算 2020 年 12 月伦敦售出的所有房产的最高价格,通过过滤表分区键中使用的列 (date) 和主键中使用的列 (town)(并且 date 不是主键的一部分)。
ClickHouse 通过应用一系列修剪技术来避免评估不相关的数据来处理该查询
① 分区修剪:MinMax 索引 用于忽略在逻辑上无法匹配查询的过滤条件的表分区键中使用的列的整个分区(一组部分)。
② 粒度修剪:在步骤 ① 之后剩余的数据块,其 主索引 用于忽略所有在逻辑上无法匹配查询的过滤条件的表主键中使用的列的 粒度(行块)。
我们可以通过 检查 示例查询的物理查询执行计划,通过 EXPLAIN 子句来观察这些数据修剪步骤。
EXPLAIN indexes = 1
SELECT MAX(price) AS highest_price
FROM uk.uk_price_paid_simple_partitioned
WHERE date >= '2020-12-01'
AND date <= '2020-12-31'
AND town = 'LONDON';
┌─explain──────────────────────────────────────────────────────────────────────────────────────────────────────┐
1. │ Expression ((Project names + Projection)) │
2. │ Aggregating │
3. │ Expression (Before GROUP BY) │
4. │ Expression │
5. │ ReadFromMergeTree (uk.uk_price_paid_simple_partitioned) │
6. │ Indexes: │
7. │ MinMax │
8. │ Keys: │
9. │ date │
10. │ Condition: and((date in (-Inf, 18627]), (date in [18597, +Inf))) │
11. │ Parts: 1/436 │
12. │ Granules: 11/3257 │
13. │ Partition │
14. │ Keys: │
15. │ toStartOfMonth(date) │
16. │ Condition: and((toStartOfMonth(date) in (-Inf, 18597]), (toStartOfMonth(date) in [18597, +Inf))) │
17. │ Parts: 1/1 │
18. │ Granules: 11/11 │
19. │ PrimaryKey │
20. │ Keys: │
21. │ town │
22. │ Condition: (town in ['LONDON', 'LONDON']) │
23. │ Parts: 1/1 │
24. │ Granules: 1/11 │
└──────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
上面的输出显示
① 分区修剪:EXPLAIN 输出的第 7 到 18 行显示 ClickHouse 首先使用 date 字段的 MinMax 索引 来识别 3257 个现有 粒度(行块)存储在 436 个现有活动数据块中,这些粒度包含与查询的 date 过滤器匹配的行。
② 粒度修剪:EXPLAIN 输出的第 19 到 24 行表明 ClickHouse 然后使用步骤 ① 中识别的数据块的主索引(在 town 字段上创建)来进一步减少粒度数量(包含可能也匹配查询的 town 过滤器的行)从 11 到 1。这也在我们打印的 ClickHouse 客户端输出中反映出来。
... Elapsed: 0.006 sec. Processed 8.19 thousand rows, 57.34 KB (1.36 million rows/s., 9.49 MB/s.)
Peak memory usage: 2.73 MiB.
这意味着 ClickHouse 扫描并处理了 1 个粒度(8192 行的块)在 6 毫秒内计算查询结果。
分区主要是一种数据管理功能
请注意,跨所有分区进行查询通常比在非分区表上运行相同的查询速度慢。
使用分区后,数据通常分布在更多的数据块中,这通常会导致 ClickHouse 扫描和处理更多的数据。
我们可以通过在 什么是表数据块 示例表(未启用分区)和我们上面的当前示例表(已启用分区)上运行相同的查询来证明这一点。这两个表 包含 相同的数据和行数。
SELECT
table,
sum(rows) AS rows
FROM system.parts
WHERE (database = 'uk') AND (table IN ['uk_price_paid_simple', 'uk_price_paid_simple_partitioned']) AND active
GROUP BY table;
但是,启用分区的表 uk_price_paid_simple_partitioned 具有更多的活动 数据块,因为,如上所述,ClickHouse 仅 合并 分区内的,而不是跨分区的数据块。
SELECT
table,
count() AS parts
FROM system.parts
WHERE (database = 'uk') AND (table IN ['uk_price_paid_simple', 'uk_price_paid_simple_partitioned']) AND active
GROUP BY table;
如上所示,分区表 uk_price_paid_simple_partitioned 具有超过 600 个分区,因此有 600 306 个活动数据块。而对于我们的非分区表 uk_price_paid_simple,所有 初始 数据块都可以通过后台合并合并到一个活动块中。
当我们 检查 使用 EXPLAIN 子句的示例查询的物理查询执行计划,该查询在已分区表上运行,我们可以看到输出的第 19 行和第 20 行,ClickHouse 识别出 3257 个现有 粒度(行块)分布在 436 个现有活动数据块中,这些粒度可能包含与查询的过滤器匹配的行,因此将被查询引擎扫描和处理。
EXPLAIN indexes = 1
SELECT MAX(price) AS highest_price
FROM uk.uk_price_paid_simple_partitioned
WHERE town = 'LONDON';
┌─explain─────────────────────────────────────────────────────────┐
1. │ Expression ((Project names + Projection)) │
2. │ Aggregating │
3. │ Expression (Before GROUP BY) │
4. │ Expression │
5. │ ReadFromMergeTree (uk.uk_price_paid_simple_partitioned) │
6. │ Indexes: │
7. │ MinMax │
8. │ Condition: true │
9. │ Parts: 436/436 │
10. │ Granules: 3257/3257 │
11. │ Partition │
12. │ Condition: true │
13. │ Parts: 436/436 │
14. │ Granules: 3257/3257 │
15. │ PrimaryKey │
16. │ Keys: │
17. │ town │
18. │ Condition: (town in ['LONDON', 'LONDON']) │
19. │ Parts: 431/436 │
20. │ Granules: 671/3257 │
└─────────────────────────────────────────────────────────────────┘
在没有分区过滤器的示例查询在没有分区表的表上运行的物理查询执行计划 显示 输出的第 11 行和第 12 行,ClickHouse 识别出表单个活动数据块中的 3083 个现有行块中的 241 个,这些行块可能包含与查询的过滤器匹配的行。
EXPLAIN indexes = 1
SELECT MAX(price) AS highest_price
FROM uk.uk_price_paid_simple
WHERE town = 'LONDON';
┌─explain───────────────────────────────────────────────┐
1. │ Expression ((Project names + Projection)) │
2. │ Aggregating │
3. │ Expression (Before GROUP BY) │
4. │ Expression │
5. │ ReadFromMergeTree (uk.uk_price_paid_simple) │
6. │ Indexes: │
7. │ PrimaryKey │
8. │ Keys: │
9. │ town │
10. │ Condition: (town in ['LONDON', 'LONDON']) │
11. │ Parts: 1/1 │
12. │ Granules: 241/3083 │
└───────────────────────────────────────────────────────┘
对于 运行 分区表版本的查询,ClickHouse 扫描并处理 671 个行块(~ 550 万行)在 90 毫秒内。
SELECT MAX(price) AS highest_price
FROM uk.uk_price_paid_simple_partitioned
WHERE town = 'LONDON';
┌─highest_price─┐
│ 594300000 │ -- 594.30 million
└───────────────┘
1 row in set. Elapsed: 0.090 sec. Processed 5.48 million rows, 27.95 MB (60.66 million rows/s., 309.51 MB/s.)
Peak memory usage: 163.44 MiB.
而对于 运行 非分区表上的查询,ClickHouse 扫描并处理 241 个行块(~ 200 万行)在 12 毫秒内。
SELECT MAX(price) AS highest_price
FROM uk.uk_price_paid_simple
WHERE town = 'LONDON';
┌─highest_price─┐
│ 594300000 │ -- 594.30 million
└───────────────┘
1 row in set. Elapsed: 0.012 sec. Processed 1.97 million rows, 9.87 MB (162.23 million rows/s., 811.17 MB/s.)
Peak memory usage: 62.02 MiB.