理解 ClickHouse 数据跳跃索引
简介
许多因素会影响 ClickHouse 的查询性能。在大多数情况下,关键要素是 ClickHouse 是否能够在评估查询 WHERE 子句条件时使用主键。因此,选择适用于最常见查询模式的主键对于有效的设计表至关重要。
然而,无论主键调整得多么精细,都不可避免地会出现无法有效利用它的查询用例。用户通常依赖 ClickHouse 处理时间序列类型的数据,但他们经常希望根据其他业务维度(例如客户 ID、网站 URL 或产品编号)分析相同的数据。在这种情况下,查询性能可能会明显下降,因为可能需要扫描每个列值的完整数据才能应用 WHERE 子句条件。虽然 ClickHouse 在这些情况下仍然相对较快,但评估数百万或数十亿个单独的值会导致“未索引”查询的执行速度远慢于基于主键的查询。
在传统的关系数据库中,一种解决此问题的方法是在表上附加一个或多个“二级”索引。这是一种 B 树结构,允许数据库在 O(log(n)) 时间内找到磁盘上所有匹配的行,而不是 O(n) 时间(表扫描),其中 n 是行数。但是,这种类型的二级索引不适用于 ClickHouse(或其他列式数据库),因为磁盘上没有单独的行可以添加到索引中。
相反,ClickHouse 提供了一种不同类型的索引,在特定情况下可以显着提高查询速度。这些结构标记为“Skip”索引,因为它们使 ClickHouse 能够跳过读取大量保证没有匹配值的的数据块。
基本操作
您只能在 MergeTree 系列的表上使用数据跳跃索引。每个数据跳跃索引都有四个主要参数
- 索引名称。索引名称用于在每个分区中创建索引文件。此外,在删除或物化索引时,它需要作为参数。
- 索引表达式。索引表达式用于计算存储在索引中的值集合。它可以是列、简单运算符和/或由索引类型确定的函数子集的组合。
- TYPE。索引类型控制确定是否可以跳过读取和评估每个索引块的计算。
- GRANULARITY。每个索引块由 GRANULARITY 个粒度组成。例如,如果主表索引的粒度为 8192 行,并且索引粒度为 4,则每个索引“块”将为 32768 行。
当用户创建数据跳跃索引时,将在表的每个数据部分目录中存在两个额外的文件。
skp_idx_{index_name}.idx,其中包含排序后的表达式值skp_idx_{index_name}.mrk2,其中包含与关联数据列文件对应的偏移量。
如果查询执行时 WHERE 子句过滤条件与跳跃索引表达式匹配,并且正在读取相关的列文件,ClickHouse 将使用索引文件数据来确定是否必须处理每个相关数据块,或者可以绕过它(假设该块尚未通过应用主键而被排除)。为了使用一个非常简化的示例,请考虑加载了可预测数据的以下表。
当执行不使用主键的简单查询时,将扫描 my_value 列中的所有 1 亿条条目
现在添加一个非常基本的跳跃索引
通常,跳跃索引仅应用于新插入的数据,因此仅添加索引不会影响上述查询。
要索引已存在的数据,请使用以下语句
使用新创建的索引重新运行查询
与处理 1 亿行 800 兆字节的数据相比,ClickHouse 仅读取和分析了 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将包含标记Thisisacandidateforfulltextsearch。它旨在用于 LIKE、EQUALS、IN、hasToken() 以及类似搜索较长字符串中的单词和其他值的搜索。例如,一种可能的用途是在应用程序日志行的自由格式列中搜索少量类名或行号。 -
专门的 ngrambf_v1。此索引的功能与 token 索引相同。它在 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 行来查找具有请求的 visitor_id 的 5 行,不如二级索引仅包含 5 个行位置,并且仅读取这 5 行。对于 ClickHouse 数据跳跃索引,情况恰恰相反。无论跳跃索引的类型如何,visitor_id 列中的所有 32768 个值都将被测试。
因此,试图通过简单地将索引添加到关键列来加速 ClickHouse 查询的自然冲动通常是不正确的。只有在调查了其他替代方案(例如修改主键(参见 如何选择主键)、使用投影或使用物化视图)之后,才应使用此高级功能。即使数据跳跃索引是合适的,仔细调整索引和表通常也是必要的。
在大多数情况下,有用的跳跃索引需要主键和目标非主键列/表达式之间存在很强的相关性。如果不存在相关性(如上图所示),则过滤条件满足块中数千个值的至少一个行的可能性很高,并且很少跳过块。相反,如果主键(如一天中的时间)的范围与潜在索引列(如电视观众年龄)的值密切相关,那么 minmax 类型的索引可能是有益的。请注意,在插入数据时,可以通过在排序/ORDER BY 键中包含其他列,或者以将与主键相关联的值分组在插入中的方式进行批处理插入,来增加这种相关性。例如,即使主键包含来自大量站点的数据,也可以将特定 site_id 的所有事件分组在一起并由 ingest 过程一起插入。这将导致许多仅包含几个 site id 的颗粒,因此在按特定 site_id 值搜索时可以跳过许多块。
跳跃索引的另一个良好候选对象是高基数表达式,其中任何一个值在数据中相对稀疏。一个例子可能是跟踪 API 请求中错误代码的可观察性平台。某些错误代码虽然在数据中很少见,但可能对搜索特别重要。在 error_code 列上的集合跳跃索引允许绕过绝大多数不包含错误的块,从而显著提高以错误为中心的查询性能。
最后,关键的最佳实践是测试、测试、再测试。再次强调,与 b 树二级索引或用于搜索文档的倒排索引不同,数据跳跃索引的行为不易预测。将其添加到表中会产生有意义的成本,包括数据 ingest 和查询,原因有很多导致查询无法从索引中受益。应始终在真实世界类型的数据上进行测试,并且测试应包括类型、粒度大小和其他参数的变化。测试通常会揭示仅从思想实验中无法明显发现的模式和陷阱。