ClickHouse 高性能实时分析数据库-索引与数据跳过(查询的“瞬移”能力)

发布于:2025-08-01 ⋅ 阅读:(15) ⋅ 点赞:(0)

告别等待,秒级响应!这不只是教程,这是你驾驭PB级数据的超能力!我的ClickHouse视频课,凝练十年实战精华,从入门到精通,从单机到集群。点开它,让数据处理速度快到飞起,让你的职业生涯从此开挂!

全套视频教程联系博主 :试听视频位置

主键索引 (稀疏索引) 的工作原理

  1. 核心概念:稀疏索引 (Sparse Index)

与 MySQL 等数据库为每一行数据都建立索引(密集索引)不同,ClickHouse 的主键索引是稀疏的。它只为每个数据颗粒(Granule)的第一行记录一个“路标”。

  • 数据颗粒 (Granule):ClickHouse 在存储数据时,会将表中的行分批打包,一个包就是一个 Granule。默认情况下,一个 Granule 包含 8192 行。

  • 索引文件 (primary.idx):这个文件非常小,因为它只存储每个 Granule 的“路标”值。例如,如果 ORDER BY(event_date),那么索引文件里存的就是每个 Granule 的起始日期。

图示

 

  1. 查询来了WHERE event_date = '2023-10-03'

  2. 扫描索引:ClickHouse 快速扫描内存中的 primary.idx 文件。

  3. 定位范围:它发现 '2023-10-03' 这个值介于路标2 ('2023-10-03') 和路标3 ('2023-10-05') 之间。这意味着,目标数据 只可能存在于 Granule 2 中

  4. 精确打击:ClickHouse 直接跳过 Granule 1 和 Granule 3,只从磁盘读取 Granule 2 这一个数据块进行处理。

结论:稀疏索引的威力在于大幅减少 I/O。它不关心数据具体在哪一行,只关心数据在哪一个数据块范围内。

主键索引的设计要点:

  • 列的选择ORDER BY 的列应该是你 WHERE 子句中最常用的过滤条件,尤其是范围查询(>, <, BETWEEN)。

  • 列的顺序:把基数更高(筛选能力更强)的列放在前面。例如 ORDER BY (event_date, user_id) 就比 ORDER BY (user_id, event_date) 要好,因为日期能先过滤掉大量不相关的数据块。

我们再强调一次:ClickHouse 的主键索引是稀疏的。它不像 MySQL 那样为每一行都建索引。它只为每个数据颗粒(Granule,默认8192行) 的第一行建立一条索引记录。

优点:索引文件非常小,可以常驻内存。 工作方式:查询时,ClickHouse 在内存中快速扫描索引,定位到可能包含目标数据的 Granule 范围,然后只把这些 Granule 从磁盘加载到内存中进行精确匹配。

【实践】: 为表添加跳数索引

给刚才的 user_behavior 表的 url 列添加一个布隆过滤器索引,以加速特定URL的查找。

-- 在建表时添加
CREATE TABLE user_behavior_with_index (
    -- ... 其他列定义和上面一样 ...
    url                 String,
    -- ...
    INDEX idx_url url TYPE bloom_filter() GRANULARITY 1 -- GRANULARITY表示索引的粒度
)
ENGINE = MergeTree()
PARTITION BY toYYYYMM(event_date)
ORDER BY (event_date, event_type, user_id);

-- 查询时,ClickHouse会自动使用该索引
-- 这个查询会因为idx_url索引而变得更快
SELECT count()
FROM user_behavior_with_index
WHERE url = 'https://clickhouse.com/docs/en/';

 数据跳过索引 (Skipping Indexes)-Granule 的“智能标签”

如果说主键索引是城市间的高速公路,那么数据跳过索引就是每个高速出口旁边的信息指示牌。它告诉你这个出口下去的区域“有什么”和“没有什么”,帮你决定是否要下高速。

数据跳过索引是附加在每个数据颗粒 (Granule) 上的元数据。它独立于主键索引,用于对非主键列进行预过滤。

除了主键,ClickHouse 还提供了额外的“跳数索引”,它们像给数据颗粒贴上的“标签”,进一步减少需要扫描的数据量。

  • minmax: 记录每个颗粒内某列的最大最小值。如果查询 WHERE price > 500,而某个颗粒的 minmax 标签是 [100, 400],则可以直接跳过。

  • set(N): 记录每个颗粒内某列的前N个唯一值。如果查询 WHERE color = 'Red',而某个颗粒的 set 标签是 {'Blue', 'Green'},则可以跳过。

  • bloom_filter: 一种概率性索引。如果你查询 WHERE has(urls, 'some_rare_url'),布隆过滤器可以快速告诉你“这个颗粒绝对没有这个URL”,从而跳过。它可能会误报(说有但实际没有),但绝不会漏报。

① minmax
  • 作用:记录每个 Granule 中某一列的最小值和最大值。

  • 场景:非常适合数值或日期类型。

  • 原理:查询 WHERE price > 1000。如果某个 Granule 的 minmax 标签是 [100, 900],ClickHouse 就知道这个 Granule 内所有 price 都小于等于900,不可能满足条件,于是直接跳过。

 

图解:查询 price > 1000 时,Granule 1 被直接跳过,因为它的最大值 900 都不满足条件。Granule 2 和 Granule 3 因为范围有交集,所以需要被读取。

② set(N)
  • 作用:记录每个 Granule 中某列的前 N 个唯一值

  • 场景:适合基数较低的 StringEnum 列,用于等值查询。

  • 原理:查询 WHERE city = 'Shanghai'。如果某个 Granule 的 set(3) 标签是 {'Beijing', 'Guangzhou', 'Shenzhen'},ClickHouse 就知道这个 Granule 里根本没有 'Shanghai',直接跳过。

③ bloom_filter
  • 作用:一种概率性数据结构,可以非常确定地判断一个元素“绝对不存在”,但只能概率性地判断“可能存在”

  • 场景

    • 高基数的 String 列(如 URL,用户ID)。

    • 检查数组中是否包含某个元素 has(array, 'value')

    • 检查 Map 中是否存在某个键 mapContains(map, 'key')

  • 原理:它像一个“黑名单筛选器”。数据写入时,把 Granule 里的值都扔进布隆过滤器。查询时,先问布隆过滤器:“这个值在你的黑名单上吗?”

    • 如果回答“不在”(即绝对不存在),则安全跳过

    • 如果回答“可能在”(有可能是误报),则需要读取 Granule 进一步确认

 

图解:查询 'e.com' 时,布隆过滤器 1 准确地告诉我们 Granule 1 中没有,从而避免了一次 I/O。布隆过滤器 2 提示可能存在,我们就需要去读取 Granule 2 来做最终的判断。

【实践】: 为表添加跳数索引

 

CREATE TABLE access_logs (
    event_time  DateTime,
    request_id  String,
    http_code   UInt16,
    url         String
) ENGINE = MergeTree()
ORDER BY (event_time)
SETTINGS index_granularity = 8192; -- 明确指定颗粒大小

-- 为 request_id 和 http_code 添加跳数索引
ALTER TABLE access_logs ADD INDEX idx_req_id request_id TYPE bloom_filter() GRANULARITY 4;
ALTER TABLE access_logs ADD INDEX idx_code http_code TYPE set(0) GRANULARITY 4;

GRANULARITY 4:表示这个跳数索引的粒度是主索引的 4 倍。即每 4 * 8192 行数据,才生成一个跳数索引块。这是一种在索引精度和大小之间的权衡。

2. 验证索引是否生效: 使用 EXPLAIN 或查询日志 system.query_log 是最好的方法。我们用一个更直观的方式:trace_logging

-- 执行带 trace_logging 的查询
SELECT count()
FROM access_logs
WHERE request_id = 'some-very-specific-request-id-abcdef'
SETTINGS log_queries=1; -- 确保查询被记录

-- 在执行查询后,立刻查看日志
-- 在 clickhouse-server.log 文件中,或者在 system.query_log 表中查找
-- 你会看到类似这样的日志:
/*
<Trace> MergeTree(Reading): Mark ranges: [0, 1]
<Trace> MergeTree(Reading): Selected 1/100 parts by partition key
<Trace> MergeTree(Reading): Selected 1/50 ranges by primary key
<Trace> MergeTree(Reading): Selected 5/20 granules by skipping indexes -- 关键!
*/

日志中的 Selected ... granules by skipping indexes 明确告诉你,数据跳过索引生效了!它帮助 ClickHouse 在主键筛选之后,又进一步排除了更多的 Granule。

总结与最佳实践

  1. 主键索引是基石ORDER BY 决定了数据的大方向,是性能优化的第一道防线。

  2. 跳数索引是精细化武器:它在主键索引筛选后的“候选范围”内,进行二次精准打击,进一步减少 I/O。

  3. 按需索骥:不要滥用索引!每个索引都会在写入时带来额外的计算开销,并占用存储空间。只为那些真正能大幅缩小查询范围的列创建索引。

  4. 如何选择?

    1. 数值/日期范围查询 -> minmax

    2. 低基数 String/Enum 等值查询 -> set

    3. 高基数 String 等值查询或 has() / mapContains() -> bloom_filter

掌握了主键索引和数据跳过索引的组合拳,你就掌握了开启 ClickHouse 极致性能的钥匙。现在,去锻造你自己的“神兵利器”吧!


网站公告

今日签到

点亮在社区的每一天
去签到