理解 ClickHouse 数据跳过索引
介绍
许多因素会影响 ClickHouse 的查询性能。在大多数情况下,关键因素是 ClickHouse 是否能够在评估查询的 WHERE 子句条件时使用主键。因此,选择适用于最常见查询模式的主键对于有效的表设计至关重要。
然而,无论主键调优得多么仔细,必然会出现无法高效使用主键的查询用例。用户通常依赖 ClickHouse 处理时间序列类型的数据,但他们往往希望根据其他业务维度分析这些同样的数据,例如客户ID、网站 URL 或产品编号。在这种情况下,查询性能可能会大幅下降,因为可能需要对每个列值进行全面扫描以应用 WHERE 子句条件。尽管在这些情况下 ClickHouse 仍然相对快速,但评估数百万或数十亿个单个值会导致“非索引”查询的执行速度远低于基于主键的查询。
在传统关系数据库中,解决此问题的一种方法是在表上附加一个或多个“次级”索引。这是一种 b-tree 结构,允许数据库以 O(log(n)) 的时间而不是 O(n) 的时间(全表扫描)找到所有匹配的行,其中 n 是行数。然而,这种类型的次级索引不适用于 ClickHouse(或其他列存储数据库),因为磁盘上没有单独的行可以添加到索引中。
相反,ClickHouse 提供了一种不同类型的索引,在特定情况下可以显著提高查询速度。这些结构被标记为“跳过”索引,因为它们使 ClickHouse 可以跳过读取保证没有匹配值的大块数据。
基本操作
用户只能在 MergeTree 家族的表上使用数据跳过索引。每个数据跳过索引有四个主要参数:
- 索引名称。索引名称用于在每个分区中创建索引文件。而且,删除或物化索引时也需要作为参数。
- 索引表达式。索引表达式用于计算存储在索引中的值集。它可以是列、简单运算符和/或由索引类型确定的函数子集的组合。
- TYPE。索引的类型控制计算,以确定是否可以跳过读取和评估每个索引块。
- GRANULARITY。每个索引块由 GRANULARITY 个小颗粒组成。例如,如果主表索引的颗粒度为 8192 行,而索引颗粒度为 4,则每个索引的“块”将为 32768 行。
当用户创建数据跳过索引时,表的每个数据部分目录中将会有两个额外的文件。
skp_idx_{index_name}.idx
,其中包含排序后的表达式值skp_idx_{index_name}.mrk2
,其中包含相应的数据列文件的偏移量。
如果在执行查询并读取相关列文件时 WHERE 子句过滤条件的某些部分匹配跳过索引表达式,ClickHouse 将使用索引文件数据来判断每个相关的数据块是否必须处理或可以跳过(假设该块尚未通过应用主键而被排除)。为了使用一个非常简单的例子,考虑以下加载了可预测数据的表。
在执行一个不使用主键的简单查询时,my_value
列中的所有 1 亿条记录都会被扫描:
现在添加一个非常基本的跳过索引:
通常跳过索引仅应用于新插入的数据,因此仅添加索引不会影响上述查询。
要对已经存在的数据进行索引,使用以下语句:
使用新创建的索引重新运行查询:
ClickHouse 处理的不是 800 兆字节的 1 亿行,而只是读取和分析了 32768 行的 360 千字节 -- 四个颗粒,每个颗粒 8192 行。
更加直观地说,这就是如何读取和选择 my_value
为 125 的 4096 行,以及如何跳过后续行而不从磁盘读取:

用户可以通过在执行查询时启用跟踪来访问关于跳过索引使用的详细信息。从 clickhouse-client 设置 send_logs_level
:
这将在尝试调优查询 SQL 和表索引时提供有用的调试信息。从上面的例子中,调试日志显示跳过索引丢弃了所有除了两个颗粒之外的块:
跳过索引类型
minmax
这种轻量级索引类型不需要参数。它存储每个块的索引表达式的最小值和最大值(如果表达式是元组,它为元组元素的每个成员单独存储值)。此类型非常适合按值松散排序的列。该索引类型通常在查询处理中应用的成本最低。
这种类型的索引仅对标量或元组表达式正确工作 -- 索引不会应用于返回数组或映射数据类型的表达式。
set
这种轻量级索引类型接受一个参数,即每个块的值集的 max_size(0 允许无限数量的离散值)。此集合包含块中的所有值(如果值的数量超过 max_size,则为空)。该索引类型在每个颗粒集合中低基数的列上效果良好(本质上,“聚集在一起”),但整体基数较高。
该索引的成本、性能和有效性取决于块内的基数。如果每个块包含大量唯一值,则评估查询条件与大型索引集的比较将非常昂贵,或者由于超过 max_size,索引将不被应用,因为索引为空。
Bloom 过滤器类型
Bloom 过滤器 是一种数据结构,允许以空间高效的方式测试集合成员资格,但有一些小的假阳性机会。在跳过索引的情况下,假阳性并不是一个重要问题,因为唯一的不利之处是读取一些不必要的块。然而,假阳性的潜在可能性确实意味着索引表达式应该被期望为真,否则有效数据可能会被跳过。
由于 Bloom 过滤器能够更高效地处理对大量离散值的测试,它们可以适用于生成更多待测试值的条件表达式。特别地,Bloom 过滤器索引可以应用于数组,其中测试数组的每个值,并且可以通过将键或值转换为数组使用 mapKeys 或 mapValues 函数来应用于映射。
基于 Bloom 过滤器的有三种数据跳过索引类型:
-
基本的 bloom_filter,接受一个可选参数,允许的“假阳性”率在 0 到 1 之间(如果没有指定,使用 .025)。
-
专用的 tokenbf_v1。它接受三个参数,均与调整使用的 Bloom 过滤器有关: (1) 过滤器的大小,以字节为单位(更大的过滤器假阳性较少,但存储成本较高), (2) 应用的哈希函数数量(同样,更多的哈希过滤器可以减少假阳性),以及 (3) Bloom 过滤器哈希函数的种子。有关这些参数如何影响 Bloom 过滤器功能的更多详细信息,请查阅计算器 在这里。 该索引仅适用于 String、FixedString 和 Map 数据类型。输入表达式按非字母数字字符分隔成字符序列。例如,列值为
This is a candidate for a "full text" search
将包含标记This
is
a
candidate
for
full
text
search
。它用于 LIKE、EQUALS、IN、hasToken() 和类似的在更长字符串内搜索单词和其他值。例如,可能的一个用法是在自由格式应用程序日志行的列中搜索少量类名或行号。 -
专用的 ngrambf_v1。该索引的功能与令牌索引相同。在 Bloom 过滤器设置之前,它还需一个附加参数,即要索引的 ngram 的大小。ngram 是长度为
n
的字符字符串,因此字符串A short string
的 ngram 大小为 4 的话将会索引为:
该索引对于文本搜索也很有用,特别是在没有单词分隔的语言中,例如中文。
跳过索引函数
数据跳过索引的核心目的在于限制热门查询所分析的数据量。考虑到 ClickHouse 数据的分析性质,这些查询的模式在大多数情况下包含功能表达式。因此,跳过索引必须与常用函数正确互动才能高效。这可以发生在:
- 插入数据并且索引被定义为功能表达式(表达式的结果存储在索引文件中),或者
- 查询被处理,并将表达式应用于存储的索引值,以决定是否排除该块。
每种跳过索引类型都基于适合索引实现的可用 ClickHouse 函数子集进行工作,列出在 这里。一般来说,集合索引和基于 Bloom 过滤器的索引(另一种类型的集合索引)都是无序的,因此不适用于范围。相比之下,minmax 索引在处理范围时特别有效,因为确定是否存在范围交集非常快速。部分匹配函数 LIKE、startsWith、endsWith 和 hasToken 的有效性取决于所使用的索引类型、索引表达式和数据的特定形状。
跳过索引设置
有两个可用于跳过索引的设置。
- use_skip_indexes (0 或 1,默认值为 1)。并非所有查询都能有效使用跳过索引。如果某个特定的过滤条件可能包括大多数颗粒,则应用数据跳过索引将产生不必要且有时是显著的成本。对于不太可能从任何跳过索引中受益的查询,将值设置为 0。
- force_data_skipping_indices (以逗号分隔的索引名称列表)。该设置可用于防止某些类型的低效查询。在查询某个表的成本太高,除非使用跳过索引的情况下,此设置与一个或多个索引名称一起使用,将返回任何未使用列出索引的查询的异常。这将防止书写不良的查询消耗服务器资源。
跳过最佳实践
跳过索引并不直观,特别是对于习惯于RDMS领域的基于行的次级索引或文档存储的倒排索引的用户。要获得任何好处,应用 ClickHouse 数据跳过索引必须避免足够的颗粒读取,以抵消计算索引的成本。至关重要的是,如果某个值在一个索引块中出现一次,那么就意味着必须将整个块读入内存并评估,从而无谓地产生了索引成本。
考虑以下数据分布:

假设主键/排序键是 timestamp
,并且 visitor_id
上有一个索引。考虑以下查询:
在这种数据分布下,传统的次级索引将非常有利。与其读取所有 32768 行以找到 5 行请求的 visitor_id,不如次级索引仅包含 5 个行位置,而只读取这 5 行的内容。对于 ClickHouse 数据跳过索引而言,情况正好相反。无论跳过索引的类型如何,visitor_id
列中的所有 32768 个值都会被测试。
因此,试图通过简单地将索引添加到关键列来加速 ClickHouse 查询的自然冲动往往是错误的。在调查其他替代方案后,例如修改主键(请参见 如何选择主键)、使用投影或使用物化视图,仅应使用此高级功能。即使数据跳过索引是合适的,通常也需要仔细调优索引和表。
在大多数情况下,有用的跳过索引需要主键与目标非主键列/表达式之间有较强的相关性。如果没有相关性(如上图所示),那么块中数千个值的行符合过滤条件的可能性就很高,因此很少会跳过块。相反,如果主键(如时间)范围与潜在的索引列(如电视观众年龄)中的值紧密相关,那么 minmax 类型的索引很可能是有利的。请注意,在插入数据时,可能通过在排序/ORDER BY 键中包含额外的列,或以将与主键相关的值分组插入的方式来增加这种相关性。例如,某个特定 site_id 的所有事件可以被聚集并一起插入,即使主键是包含来自大量站点的事件的时间戳。这将导致许多颗粒仅包含几个 site_id,因此在按特定 site_id 值搜索时可以跳过许多块。
跳过索引的另一个良好候选者是对于高基数表达式,其中任何一个值在数据中相对稀疏。例如,一个观察平台跟踪 API 请求中的错误代码。虽然某些错误代码在数据中很少见,但可能在搜索中尤其重要。在 error_code 列上使用集合跳过索引将允许绕过绝大多数不包含错误的块,从而显著改善以错误为重点的查询。
最后,关键的最佳实践是不断测试。与寻找文档的 b-tree 次级索引或倒排索引不同,数据跳过索引的行为并不容易预测。将它们添加到表中会在数据摄取时和任何由于多种原因而未从索引中受益的查询中产生实际的成本。它们应始终在现实世界类型的数据上进行测试,测试还应包括类型、颗粒大小和其他参数的变化。测试通常会揭示出仅通过思考实验而不明显的模式和陷阱。