想了解更进阶的索引内容?
本页介绍 ClickHouse 的稀疏主索引,包括其构建方式、工作原理,以及如何提升查询性能。
如需了解更进阶的索引策略和更深入的技术细节,请参阅主索引深度剖析。
ClickHouse 中的稀疏主键索引是如何工作的?
ClickHouse 中的稀疏主键索引用于高效定位粒度(granules)——由多行组成的数据块——这些数据块可能包含满足查询在表 ^^primary key^^ 列上条件的数据。下一节将说明该索引是如何根据这些列中的值构建的。
稀疏主索引的创建
为了演示稀疏主索引是如何构建的,我们会结合一些动画来说明 uk_price_paid_simple 表。
作为回顾,在我们的示例表①中,^^primary key^^ 为 (town, street),② 插入的数据会被③存储到磁盘上,根据 ^^primary key^^ 列的取值排序并压缩,每一列单独存储在各自的文件中:
在处理时,每一列的数据会被④在逻辑上划分为多个 granule——每个 granule 覆盖 8,192 行——这是 ClickHouse 数据处理机制所使用的最小单元。
这种 ^^granule^^ 结构也是主索引之所以是稀疏的:ClickHouse 并不会为每一行建立索引,而是只存储每个 ^^granule^^ 中一行的 ^^primary key^^ 值——具体来说,是第一行的值。这样每个 ^^granule^^ 就对应一个索引项:
得益于这种稀疏性,主索引足够小,可以完整地放入内存中,从而为在 ^^primary key^^ 列上带有谓词条件的查询提供快速过滤。在下一节中,我们将展示它如何帮助加速此类查询。
主索引的使用
下面通过另一段动画概述稀疏主索引是如何用于加速查询的:
① 示例查询在两个 ^^primary key^^ 列上都包含过滤条件:town = 'LONDON' AND street = 'OXFORD STREET'。
② 为了加速查询,ClickHouse 会将表的主索引加载到内存中。
③ 随后扫描索引项,以确定哪些 granule 可能包含满足过滤条件的行——换句话说,哪些 granule 不能被跳过。
④ 然后将这些潜在相关的 granule 以及查询所需其他列中对应的 granule 一并加载到内存中并进行处理。
监控主索引
表中的每个数据部分都有自己的主索引。我们可以使用 mergeTreeIndex 表函数来查看这些索引的内容。
下面的查询会列出示例表中每个数据部分的主索引中的条目数量:
SELECT
part_name,
max(mark_number) AS entries
FROM mergeTreeIndex('uk', 'uk_price_paid_simple')
GROUP BY part_name;
┌─part_name─┬─entries─┐
1. │ all_2_2_0 │ 914 │
2. │ all_1_1_0 │ 1343 │
3. │ all_0_0_0 │ 1349 │
└───────────┴─────────┘
此查询显示了当前某个数据 ^^parts^^ 的主索引中的前 10 条记录。请注意,这些 ^^parts^^ 会在后台持续被合并为更大的 ^^parts^^:
SELECT
mark_number + 1 AS entry,
town,
street
FROM mergeTreeIndex('uk', 'uk_price_paid_simple')
WHERE part_name = (SELECT any(part_name) FROM mergeTreeIndex('uk', 'uk_price_paid_simple'))
ORDER BY mark_number ASC
LIMIT 10;
┌─entry─┬─town───────────┬─street───────────┐
1. │ 1 │ ABBOTS LANGLEY │ ABBEY DRIVE │
2. │ 2 │ ABERDARE │ RICHARDS TERRACE │
3. │ 3 │ ABERGELE │ PEN Y CAE │
4. │ 4 │ ABINGDON │ CHAMBRAI CLOSE │
5. │ 5 │ ABINGDON │ THORNLEY CLOSE │
6. │ 6 │ ACCRINGTON │ MAY HILL CLOSE │
7. │ 7 │ ADDLESTONE │ HARE HILL │
8. │ 8 │ ALDEBURGH │ LINDEN ROAD │
9. │ 9 │ ALDERSHOT │ HIGH STREET │
10. │ 10 │ ALFRETON │ ALMA STREET │
└───────┴────────────────┴──────────────────┘
最后,我们使用 EXPLAIN 子句来查看如何利用所有数据 ^^parts^^ 的主索引来跳过那些不可能包含满足示例查询谓词的行的 granule。这些 granule 将不会被加载和处理:
EXPLAIN indexes = 1
SELECT
max(price)
FROM
uk.uk_price_paid_simple
WHERE
town = 'LONDON' AND street = 'OXFORD STREET';
┌─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. │ street │
11. │ Condition: and((street in ['OXFORD STREET', 'OXFORD STREET']), (town in ['LONDON', 'LONDON'])) │
12. │ Parts: 3/3 │
13. │ Granules: 3/3609 │
└────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
请注意,上面 EXPLAIN 输出的第 13 行显示,在所有数据 ^^parts^^ 中的 3,609 个 granule 里,只有 3 个被主键索引的分析选中参与处理。其余 granule 则被完全跳过。
我们还可以通过直接运行该查询来观察到,大部分数据都会被跳过:
SELECT max(price)
FROM uk.uk_price_paid_simple
WHERE (town = 'LONDON') AND (street = 'OXFORD STREET');
┌─max(price)─┐
1. │ 263100000 │ -- 2.631 亿
└────────────┘
返回 1 行。耗时:0.010 秒。已处理 24.58 千行,159.04 KB(253 万行/秒,16.35 MB/秒)。
峰值内存使用:13.00 MiB。
如上所示,在该示例表的大约 3,000 万行记录中,实际只处理了约 25,000 行:
SELECT count() FROM uk.uk_price_paid_simple;
┌──count()─┐
1. │ 29556244 │ -- 2956 万
└──────────┘
关键要点
-
稀疏主索引(sparse primary index) 帮助 ClickHouse 通过识别哪些 ^^granule^^ 可能包含满足查询条件的 ^^primary key^^ 列的行,从而跳过不必要的数据。
-
每个索引只存储 每个 ^^granule^^ 的首行的 ^^primary key^^ 值(一个 ^^granule^^ 默认包含 8,192 行),因此足够紧凑,可以完全放入内存。
-
^^MergeTree^^ 表中的 每个数据 part 都有其 自己的主索引,并在查询执行期间独立使用。
-
在查询过程中,索引使 ClickHouse 能够 跳过 ^^granule^^,从而降低 I/O 和内存使用,同时提升性能。
-
你可以使用 mergeTreeIndex 表函数来 检查索引内容,并通过 EXPLAIN 子句监控索引使用情况。
若想更深入了解 ClickHouse 中稀疏主键索引的工作机制,包括其与传统数据库索引的差异以及使用它们的最佳实践,请查阅我们的索引深度解析。
如果你对 ClickHouse 如何以高度并行的方式处理经主键索引扫描选取的数据感兴趣,请参阅查询并行性指南。