跳到主要内容
跳到主要内容
编辑此页

了解 ClickHouse 数据跳过索引

简介

许多因素会影响 ClickHouse 查询性能。在大多数情况下,关键要素是 ClickHouse 在评估查询 WHERE 子句条件时是否可以使用主键。因此,选择适用于最常见查询模式的主键对于有效的表设计至关重要。

然而,无论主键调整得多么仔细,都不可避免地会出现无法有效使用它的查询用例。用户通常依赖 ClickHouse 处理时间序列类型的数据,但他们也经常希望根据其他业务维度(例如客户 ID、网站 URL 或产品编号)来分析相同的数据。在这种情况下,查询性能可能会显著下降,因为可能需要完全扫描每个列值才能应用 WHERE 子句条件。虽然 ClickHouse 在这些情况下仍然相对较快,但评估数百万或数十亿个单独的值将导致“非索引”查询的执行速度远慢于基于主键的查询。

在传统的关系数据库中,解决此问题的一种方法是将一个或多个“二级”索引附加到表。这是一种 b 树结构,允许数据库在 O(log(n)) 时间内找到磁盘上所有匹配的行,而不是 O(n) 时间(表扫描),其中 n 是行数。但是,这种类型的二级索引不适用于 ClickHouse(或其他面向列的数据库),因为磁盘上没有要添加到索引的单个行。

相反,ClickHouse 提供了一种不同类型的索引,在特定情况下可以显著提高查询速度。这些结构被标记为“跳过”索引,因为它们使 ClickHouse 能够跳过读取保证没有匹配值的大量数据块。

基本操作

用户只能在 MergeTree 系列表上使用数据跳过索引。每个数据跳过索引都有四个主要参数

  • 索引名称。索引名称用于在每个分区中创建索引文件。此外,在删除或物化索引时,它也是必需的参数。
  • 索引表达式。索引表达式用于计算存储在索引中的值集。它可以是列的组合、简单运算符和/或索引类型确定的函数子集。
  • 类型。索引类型控制计算,该计算确定是否可以跳过读取和评估每个索引块。
  • 粒度。每个索引块由粒度个 granules 组成。例如,如果主表索引的粒度为 8192 行,并且索引粒度为 4,则每个索引“块”将为 32768 行。

当用户创建数据跳过索引时,每个表数据 part 目录中将有两个附加文件。

  • skp_idx_{index_name}.idx,其中包含有序的表达式值
  • skp_idx_{index_name}.mrk2,其中包含关联数据列文件的相应偏移量。

如果 WHERE 子句过滤条件的某些部分与执行查询和读取相关列文件时的跳过索引表达式匹配,ClickHouse 将使用索引文件数据来确定是否必须处理每个相关数据块或可以绕过(假设该块尚未通过应用主键排除)。为了使用一个非常简化的示例,请考虑以下加载了可预测数据的表。

CREATE TABLE skip_table
(
my_key UInt64,
my_value UInt64
)
ENGINE MergeTree primary key my_key
SETTINGS index_granularity=8192;

INSERT INTO skip_table SELECT number, intDiv(number,4096) FROM numbers(100000000);

当执行不使用主键的简单查询时,将扫描 my_value 列中的所有 1 亿条条目

SELECT * FROM skip_table WHERE my_value IN (125, 700)

┌─my_key─┬─my_value─┐
│ 512000 │ 125 │
│ 512001 │ 125 │
│ ... | ... |
└────────┴──────────┘

8192 rows in set. Elapsed: 0.079 sec. Processed 100.00 million rows, 800.10 MB (1.26 billion rows/s., 10.10 GB/s.

现在添加一个非常基本跳过索引

ALTER TABLE skip_table ADD INDEX vix my_value TYPE set(100) GRANULARITY 2;

通常,跳过索引仅应用于新插入的数据,因此仅添加索引不会影响上述查询。

要为已存在的数据建立索引,请使用以下语句

ALTER TABLE skip_table MATERIALIZE INDEX vix;

使用新创建的索引重新运行查询

SELECT * FROM skip_table WHERE my_value IN (125, 700)

┌─my_key─┬─my_value─┐
│ 512000 │ 125 │
│ 512001 │ 125 │
│ ... | ... |
└────────┴──────────┘

8192 rows in set. Elapsed: 0.051 sec. Processed 32.77 thousand rows, 360.45 KB (643.75 thousand rows/s., 7.08 MB/s.)

ClickHouse 不再处理 1 亿行 800 兆字节的数据,而是仅读取和分析了 32768 行 360 千字节的数据——每个 8192 行的四个 granules。

以更直观的形式,这就是如何读取和选择 my_value 为 125 的 4096 行,以及如何跳过以下行而不从磁盘读取

Simple Skip

用户可以通过在执行查询时启用跟踪来访问有关跳过索引使用情况的详细信息。从 clickhouse-client,设置 send_logs_level

SET send_logs_level='trace';

当尝试调整查询 SQL 和表索引时,这将提供有用的调试信息。从上面的示例中,调试日志显示跳过索引丢弃了除两个 granules 之外的所有 granules

<Debug> default.skip_table (933d4b2c-8cea-4bf9-8c93-c56e900eefd1) (SelectExecutor): Index `vix` has dropped 6102/6104 granules.

跳过索引类型

minmax

这种轻量级索引类型不需要任何参数。它存储每个块的索引表达式的最小值和最大值(如果表达式是元组,则它会分别存储元组元素每个成员的值)。此类型非常适合按值松散排序的列。这种索引类型通常是在查询处理期间应用成本最低的。

这种类型的索引仅适用于标量或元组表达式——索引永远不会应用于返回数组或 map 数据类型的表达式。

set

这种轻量级索引类型接受每个块的值集的最大大小的单个参数(0 允许无限数量的离散值)。此集合包含块中的所有值(如果值的数量超过 max_size,则为空)。这种索引类型适用于每个 granules 集合中基数较低(本质上是“聚集在一起”)但总体基数较高的列。

此索引的成本、性能和有效性取决于块内的基数。如果每个块包含大量唯一值,则针对大型索引集评估查询条件将非常昂贵,或者由于超出 max_size 而索引为空,因此不会应用索引。

Bloom Filter 类型

Bloom 过滤器是一种数据结构,允许以空间高效的方式测试集合成员资格,但会略微增加误报的可能性。误报在跳过索引的情况下不是一个重要的问题,因为唯一的缺点是读取一些不必要的块。但是,误报的可能性确实意味着索引表达式应该有望为真,否则可能会跳过有效数据。

由于 Bloom 过滤器可以更有效地处理大量离散值的测试,因此它们可能适用于产生更多值进行测试的条件表达式。特别是,Bloom 过滤器索引可以应用于数组(其中测试数组的每个值)和 map(通过使用 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。此索引的功能与 token 索引相同。它在 Bloom 过滤器设置之前采用一个附加参数,即要索引的 n-gram 的大小。n-gram 是长度为 n 的任意字符的字符串,因此字符串 A short string 的 n-gram 大小为 4 将被索引为

    'A sh', ' sho', 'shor', 'hort', 'ort ', 'rt s', 't st', ' str', 'stri', 'trin', 'ring'

此索引也可用于文本搜索,特别是对于没有单词分隔符的语言(例如中文)。

跳过索引函数

数据跳过索引的核心目的是限制常用查询分析的数据量。鉴于 ClickHouse 数据的分析性质,大多数情况下这些查询的模式都包含函数表达式。因此,跳过索引必须与常用函数正确交互才能有效。这可能发生在以下两种情况之一:

  • 插入数据并且索引被定义为函数表达式(表达式的结果存储在索引文件中),或者
  • 处理查询并将表达式应用于存储的索引值,以确定是否排除该块。

每种类型的跳过索引都适用于 此处 列出的适用于索引实现的可用 ClickHouse 函数的子集。通常,set 索引和基于 Bloom 过滤器的索引(另一种类型的 set 索引)都是无序的,因此不适用于范围。相比之下,minmax 索引特别适用于范围,因为确定范围是否相交非常快。部分匹配函数 LIKE、startsWith、endsWith 和 hasToken 的有效性取决于使用的索引类型、索引表达式和数据的特定形状。

跳过索引设置

有两个可用于跳过索引的设置。

  • use_skip_indexes(0 或 1,默认值为 1)。并非所有查询都可以有效地使用跳过索引。如果特定的过滤条件可能包含大多数 granules,则应用数据跳过索引会产生不必要的,有时甚至是巨大的成本。对于不太可能从任何跳过索引中受益的查询,请将该值设置为 0。
  • force_data_skipping_indices(逗号分隔的索引名称列表)。此设置可用于防止某些类型的低效查询。在查询表成本太高除非使用跳过索引的情况下,将此设置与一个或多个索引名称一起使用将为任何未使用列出的索引的查询返回异常。这将防止编写不佳的查询消耗服务器资源。

跳过最佳实践

跳过索引并不直观,尤其是对于习惯于 RDMS 领域的基于行的二级索引或文档存储的倒排索引的用户而言。为了获得任何好处,应用 ClickHouse 数据跳过索引必须避免读取足够的 granules 以抵消计算索引的成本。至关重要的是,如果一个值在一个索引块中出现一次,则意味着必须将整个块读取到内存并进行评估,并且不必要地产生了索引成本。

考虑以下数据分布

Bad Skip!

假设主键/排序键是 timestamp,并且 visitor_id 上有一个索引。考虑以下查询

SELECT timestamp, url FROM table WHERE visitor_id = 1001

传统的二级索引对于这种数据分布将非常有利。二级索引将仅包含五个行位置,而不是读取所有 32768 行以查找具有请求的 visitor_id 的 5 行,并且仅将从磁盘读取这五行。对于 ClickHouse 数据跳过索引而言,情况恰恰相反。无论跳过索引的类型如何,都将测试 visitor_id 列中的所有 32768 个值。

因此,简单地通过向键列添加索引来尝试加速 ClickHouse 查询的自然冲动通常是不正确的。只有在研究其他替代方案之后,才应使用此高级功能,例如修改主键(请参阅如何选择主键)、使用 projections 或使用物化视图。即使数据跳过索引是合适的,通常也需要仔细调整索引和表。

在大多数情况下,有用的跳过索引需要在主键和目标非主键列/表达式之间建立强相关性。如果不存在相关性(如上图所示),则在数千个值的块中,至少一个行满足过滤条件的可能性很高,并且将跳过很少的块。相反,如果主键的值范围(如一天中的时间)与潜在索引列中的值(例如电视观众年龄)密切相关,则 minmax 类型的索引可能是有益的。请注意,在插入数据时,可以通过在排序/ORDER BY 键中包含其他列,或者以与主键关联的值在插入时分组的方式批量插入来增加这种相关性。例如,即使主键是包含来自大量站点的事件的时间戳,也可以通过提取过程将特定 site_id 的所有事件分组并一起插入。这将导致许多 granules 仅包含几个站点 ID,因此在按特定 site_id 值搜索时可以跳过许多块。

跳过索引的另一个良好候选者是高基数表达式,其中任何一个值在数据中相对稀疏。一个示例可能是可观察性平台,该平台跟踪 API 请求中的错误代码。某些错误代码虽然在数据中很少见,但对于搜索可能特别重要。error_code 列上的 set 跳过索引将允许绕过绝大多数不包含错误的块,从而显著改进以错误为中心的查询。

最后,关键的最佳实践是测试、测试、再测试。同样,与用于搜索文档的 b 树二级索引或倒排索引不同,数据跳过索引的行为不容易预测。将它们添加到表中会在数据提取和任何出于任何原因都无法从索引中受益的查询上产生有意义的成本。应始终在真实世界类型的数据上对其进行测试,并且测试应包括类型、粒度大小和其他参数的变化。测试通常会揭示模式和陷阱,这些模式和陷阱仅凭思想实验是无法显而易见的。