理解 ClickHouse 数据跳过索引
介绍
许多因素会影响 ClickHouse 查询性能。在大多数情况下,一个关键因素是 ClickHouse 是否可以在评估查询 WHERE 子句条件时使用主键。因此,选择适用于最常见查询模式的主键对于有效的表设计至关重要。
然而,无论主键调整得多么仔细,总会不可避免地出现无法有效利用它的查询用例。用户通常依赖 ClickHouse 处理时间序列类型数据,但他们也经常希望根据其他业务维度(如客户 ID、网站 URL 或产品编号)分析这些数据。在这种情况下,查询性能可能会大大降低,因为可能需要对每个列值进行完全扫描以应用 WHERE 子句条件。虽然 ClickHouse 在这种情况下仍然相对快速,但评估数百万或数十亿个单独值会导致“非索引”查询的执行速度远远低于基于主键的查询。
在传统的关联数据库中,解决此问题的一种方法是在表上附加一个或多个“二级”索引。这是一种 B 树结构,允许数据库在 O(log(n)) 时间内而不是 O(n) 时间内(表扫描)找到所有匹配的行,其中 n 是行数。但是,这种类型的二级索引不适用于 ClickHouse(或其他列式数据库),因为磁盘上没有单独的行可以添加到索引中。
相反,ClickHouse 提供了不同类型的索引,在特定情况下可以显著提高查询速度。这些结构被称为“跳过”索引,因为它们允许 ClickHouse 跳过读取大量数据,这些数据保证不包含匹配的值。
基本操作
用户只能在 MergeTree 系列表上使用数据跳过索引。每个数据跳过都有四个主要参数
- 索引名称。索引名称用于在每个分区中创建索引文件。此外,它也作为参数在删除或物化索引时需要。
- 索引表达式。索引表达式用于计算存储在索引中的值集。它可以是列、简单运算符和/或索引类型确定的函数子集的组合。
- 类型。索引类型控制用于确定是否可以跳过读取和评估每个索引块的计算。
- 粒度。每个索引块由粒度数量的颗粒组成。例如,如果主表的索引粒度为 8192 行,而索引粒度为 4,则每个索引“块”将包含 32768 行。
当用户创建数据跳过索引时,表中每个数据部分目录中将有两个额外的文件。
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 没有处理 800 MB 的 1 亿行,而是只读取和分析了 360 KB 的 32768 行——每个 8192 行的四个颗粒。
以更直观的形式,以下是读取和选择具有 my_value
为 125 的 4096 行,以及如何在不从磁盘读取的情况下跳过后续行的过程
用户可以通过在执行查询时启用跟踪来访问有关跳过索引使用的详细信息。从 clickhouse-client 中,设置 send_logs_level
SET send_logs_level='trace';
在尝试调整查询 SQL 和表索引时,这将提供有用的调试信息。从上面的示例中,调试日志显示跳过索引丢弃了除两个颗粒以外的所有颗粒
<Debug> default.skip_table (933d4b2c-8cea-4bf9-8c93-c56e900eefd1) (SelectExecutor): Index `vix` has dropped 6102/6104 granules.
跳过索引类型
minmax
这种轻量级索引类型不需要任何参数。它存储每个块中索引表达式的最小值和最大值(如果表达式是元组,则分别存储元组元素的每个成员的值)。这种类型非常适合那些按值大致排序的列。这种索引类型通常是最便宜的,因为它在查询处理过程中应用。
这种类型的索引仅适用于标量或元组表达式——索引永远不会应用于返回数组或映射数据类型的表达式。
set
这种轻量级索引类型接受一个参数,即每个块的值集的最大大小(0 允许无限数量的离散值)。此集合包含块中的所有值(或如果值数量超过最大大小,则为空)。这种索引类型适用于在每个颗粒集中具有低基数(本质上是“聚集在一起”)但在整体上具有较高基数的列。
该索引的成本、性能和有效性取决于块中的基数。如果每个块都包含大量唯一的值,那么要么评估查询条件与大型索引集将非常昂贵,要么索引将不会应用,因为索引由于超过最大大小而为空。
布隆过滤器类型
布隆过滤器是一种数据结构,允许以空间效率的方式测试集合成员资格,但代价是存在轻微的假阳性率。在跳过索引的情况下,假阳性率并非严重问题,因为唯一的缺点是读取一些不必要的块。但是,假阳性率的可能性确实意味着应预计索引表达式为真,否则有效数据可能会被跳过。
由于布隆过滤器可以更有效地处理对大量离散值的测试,因此它们可能适用于产生更多要测试的值的条件表达式。特别是,布隆过滤器索引可以应用于数组,其中测试数组的每个值,以及映射,通过使用 mapKeys 或 mapValues 函数将键或值转换为数组。
有三种基于布隆过滤器的 Data Skipping Index 类型
基本的 bloom_filter,它接受一个可选的参数,即允许的“假阳性”率,介于 0 和 1 之间(如果未指定,则使用 .025)。
专用的 tokenbf_v1。它接受三个参数,所有参数都与调整所使用的布隆过滤器有关:(1) 过滤器的字节大小(较大的过滤器具有更少的误报,但存储成本更高),(2) 应用的哈希函数数量(同样,更多哈希过滤器减少误报),以及 (3) 布隆过滤器哈希函数的种子。有关这些参数如何影响布隆过滤器功能的更多详细信息,请参阅 此处 的计算器。此索引仅适用于 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。此索引的功能与标记索引相同。它在布隆过滤器设置之前接受一个额外的参数,即要索引的 ngram 的大小。ngram 是长度为
n
的任意字符的字符字符串,因此字符串A short string
的 ngram 大小为 4 将被索引为'A sh', ' sho', 'shor', 'hort', 'ort ', 'rt s', 't st', ' str', 'stri', 'trin', 'ring'
此索引对于文本搜索也很有用,尤其是对于没有词语分隔符的语言,例如中文。
跳过索引函数
数据跳过索引的核心目的是限制流行查询分析的数据量。鉴于 ClickHouse 数据的分析性质,大多数情况下这些查询的模式包括函数表达式。因此,跳过索引必须与常用函数正确交互才能高效。这种情况可能发生在
- 数据插入且索引定义为函数表达式(将表达式的结果存储在索引文件中)时,或者
- 处理查询并将表达式应用于存储的索引值以确定是否排除块时。
每种类型的跳过索引都在适用于索引实现的可用 ClickHouse 函数子集上工作,列于 此处。通常,集合索引和基于布隆过滤器的索引(另一种类型的集合索引)都是无序的,因此不适用于范围。相反,最小值/最大值索引特别适合范围,因为确定范围是否相交非常快。部分匹配函数 LIKE、startsWith、endsWith 和 hasToken 的有效性取决于所使用的索引类型、索引表达式和数据的特定形状。
跳过索引设置
有两个可用的设置适用于跳过索引。
- use_skip_indexes(0 或 1,默认值为 1)。并非所有查询都能有效地使用跳过索引。如果某个特定的过滤条件可能包含大多数颗粒,应用数据跳过索引会产生不必要且有时很高的成本。对于不太可能从任何跳过索引中受益的查询,将该值设置为 0。
- force_data_skipping_indices(以逗号分隔的索引名称列表)。此设置可用于防止某些类型的低效查询。在查询表过于昂贵(除非使用跳过索引)的情况下,将此设置与一个或多个索引名称一起使用将为任何不使用所列索引的查询返回异常。这将阻止编写不当的查询消耗服务器资源。
跳过最佳实践
跳过索引并不直观,尤其是对于习惯于 RDMS 领域的基于行的二级索引或文档存储中的倒排索引的用户而言。要获得任何好处,应用 ClickHouse 数据跳过索引必须避免足够的颗粒读取以抵消计算索引的成本。至关重要的是,如果某个值在一个索引块中出现一次,则意味着必须将整个块读取到内存中并进行评估,并且索引成本已经不必要地产生。
考虑以下数据分布
假设主/排序键是 timestamp
,并且在 visitor_id
上存在一个索引。考虑以下查询
SELECT timestamp, url FROM table WHERE visitor_id = 1001
对于这种数据分布,传统的二级索引将非常有利。二级索引不会读取所有 32768 行以找到具有请求的 visitor_id 的 5 行,而是只包含 5 个行位置,并且只从磁盘读取这 5 行。对于 ClickHouse 数据跳过索引来说,情况恰恰相反。将测试 visitor_id
列中的所有 32768 个值,而不管跳过索引的类型如何。
因此,通过简单地向键列添加索引来尝试加速 ClickHouse 查询的自然冲动通常是错误的。只有在调查了其他替代方案(例如修改主鍵(请参阅 如何选择主鍵)、使用投影或使用物化视图)之后,才应使用此高级功能。即使数据跳过索引适用,也通常需要仔细调整索引和表。
在大多数情况下,有用的跳过索引需要主鍵和目标非主列/表达式之间存在很强的相关性。如果不存在相关性(如上图所示),则过滤条件在几千个值的块中至少有一行满足的可能性很高,并且只有很少的块会被跳过。相反,如果主鍵值范围(如一天中的时间)与潜在索引列中的值(如电视观众年龄)密切相关,那么最小值/最大值类型的索引可能会有益。请注意,在插入数据时可以增加这种相关性,方法是在排序/ORDER BY 鍵中包含更多列,或者以一种将与主鍵相关联的值分组插入的方式进行批量插入。例如,即使主鍵是包含来自大量站点的事件的时间戳,但特定站点 ID 的所有事件都可以在提取过程中分组并一起插入。这将导致许多仅包含少量站点 ID 的颗粒,因此在按特定站点 ID 值搜索时,可以跳过许多块。
跳过索引的另一个适用场景是高基数表达式,其中任何单个值在数据中都相对稀疏。一个例子可能是跟踪 API 请求中的错误代码的观测平台。某些错误代码虽然在数据中很少见,但对于搜索可能特别重要。在 error_code 列上建立集合跳过索引可以绕过绝大多数不包含错误的块,从而显着改善以错误为中心的查询。
最后,关键的最佳实践是测试、测试、测试。再次强调,与用于搜索文档的 B 树二级索引或倒排索引不同,数据跳过索引的行为不容易预测。将它们添加到表中会对数据提取和由于各种原因无法从索引中受益的查询产生有意义的成本。它们应始终在现实类型的数据上进行测试,并且测试应包括类型、粒度大小和其他参数的变化。测试通常会揭示出仅凭思想实验无法明显看出的一些模式和陷阱。