文章目录
- 一、开篇:一个常见的困惑
- 二、什么是稀疏索引?
-
- [2.1 一句话定义](#2.1 一句话定义)
- [2.2 图解:稀疏索引的物理结构](#2.2 图解:稀疏索引的物理结构)
- 三、稀疏索引如何工作?
-
- [3.1 查询执行流程](#3.1 查询执行流程)
- [3.2 范围查询的优势](#3.2 范围查询的优势)
- [四、为什么 ClickHouse 选择稀疏索引?](#四、为什么 ClickHouse 选择稀疏索引?)
- [五、密集索引 vs 稀疏索引:量化对比](#五、密集索引 vs 稀疏索引:量化对比)
- 六、优化稀疏索引的最佳实践
-
- [6.1 把高频过滤列放在 `ORDER BY` 最前面](#6.1 把高频过滤列放在
ORDER BY最前面) - [6.2 避免点查,改用范围查询](#6.2 避免点查,改用范围查询)
- [6.3 合理设置 `index_granularity`](#6.3 合理设置
index_granularity)
- [6.1 把高频过滤列放在 `ORDER BY` 最前面](#6.1 把高频过滤列放在
- 七、常见误区与澄清
- 八、总结
在 MySQL 中,我们习惯了 B-Tree 索引的"精准打击"------一条查询瞬间定位到行。但在 ClickHouse 里,你会发现索引完全不是一回事:没有 B-Tree,没有行级定位,甚至索引文件小得可以忽略不计。这就是稀疏索引。本文将深入解析稀疏索引的设计原理、工作方式,以及它为什么更适合 OLAP 场景。
一、开篇:一个常见的困惑
很多从 MySQL 转过来的开发者,第一次接触 ClickHouse 时会问:
"为什么我的
WHERE id = 123查询还是慢?""为什么我建了索引,但好像没起作用?"
答案是:ClickHouse 的索引不是 B-Tree,而是稀疏索引。
这两种索引的设计目标完全不同:
| 对比项 | MySQL(B-Tree 密集索引) | ClickHouse(稀疏索引) |
|---|---|---|
| 记录粒度 | 每行都记录一个索引项 | 每隔 N 行(默认 8192 行)记录一个索引项 |
| 索引大小 | 大(约数据量的 10%) | 极小(约数据量的 0.1%) |
| 定位精度 | 直接定位到具体行 | 定位到数据块(granule),块内扫描 |
| 适用场景 | 点查 WHERE id = 123 |
范围查询、扫描大量行 |
| 写入代价 | 高(需维护索引树) | 低(只需追加索引条目) |
二、什么是稀疏索引?
2.1 一句话定义
稀疏索引是一个"跳着看"的目录:每隔 8192 行,记录一下这一行数据的位置和主键值。
2.2 图解:稀疏索引的物理结构
┌─────────────────────────────────────────────────────────────────────────────┐
│ 数据文件(按主键排序) │
├─────────────────────────────────────────────────────────────────────────────┤
│ Granule 0 │ Granule 1 │ Granule 2 │ ... │
│ 行1 ~ 行8192 │ 行8193 ~ 行16384 │ 行16385 ~ 行24576 │ │
│ 主键值: 1~100 │ 主键值: 101~200 │ 主键值: 201~300 │ │
└─────────────────────────────────────────────────────────────────────────────┘
│ │ │
▼ ▼ ▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ 索引文件(稀疏索引) │
├─────────────────────────────────────────────────────────────────────────────┤
│ 条目0: (主键最小值=1, 位置偏移=0) │
│ 条目1: (主键最小值=101, 位置偏移=8192行) │
│ 条目2: (主键最小值=201, 位置偏移=16384行) │
│ ... │
└─────────────────────────────────────────────────────────────────────────────┘
关键点:
- 索引不记录每一行,只记录每个 granule(8192 行)的起始位置 和主键最小值
- 索引大小 ≈ 数据量 / 8192,不到数据量的 0.1%
三、稀疏索引如何工作?
3.1 查询执行流程
以 WHERE id = 105 为例:
sql
SELECT * FROM table WHERE id = 105;
| 步骤 | 操作 | 说明 |
|---|---|---|
| 1 | 二分查找索引 | 在索引中找到 id >= 105 的第一个 granule --- 条目1(主键最小值=101) |
| 2 | 定位到 granule | 根据条目1的位置偏移,定位到 Granule 1(行8193~行16384) |
| 3 | 块内扫描 | 在 Granule 1 内逐行扫描,找到 id = 105 的行 |
结论 :稀疏索引只能帮你快速跳过不相关的 granule,但无法精确到行。granule 内的扫描是线性的。
3.2 范围查询的优势
WHERE id BETWEEN 150 AND 250:
| 步骤 | 操作 |
|---|---|
| 1 | 二分查找找到起始 granule(条目1,最小值=101) |
| 2 | 从 Granule 1 开始,连续读取 Granule 2、Granule 3... 直到 id > 250 |
| 3 | 在读取的每个 granule 内过滤数据 |
优势:连续读取多个 granule,充分利用磁盘顺序读性能,非常高效。
四、为什么 ClickHouse 选择稀疏索引?
| 设计目标 | 为什么选稀疏索引 |
|---|---|
| 极低存储开销 | 索引大小 ≈ 数据量的 0.1%,几乎可以忽略 |
| 快速写入 | 无需维护复杂的 B-Tree 结构,直接追加索引条目 |
| 支持海量数据 | 10 亿行数据,索引只有约 12 万条记录 |
| 适合范围扫描 | 一次性读取连续 granule,磁盘顺序读性能极佳 |
| 列式扫描友好 | granule 是列式压缩的基本单位,一次读取一个完整压缩块 |
代价:
- 点查性能差(需要 granule 内扫描)
- 不适合高并发点查场景
五、密集索引 vs 稀疏索引:量化对比
假设一张 10 亿行、主键为 user_id 的表:
| 对比项 | MySQL(B-Tree 密集索引) | ClickHouse(稀疏索引) |
|---|---|---|
| 索引条目数 | 10 亿条 | ≈ 122,000 条(10亿 / 8192) |
| 索引大小 | ≈ 20GB | ≈ 2MB |
WHERE id = 123 |
3~4 次磁盘 I/O | 定位 granule + 扫描 8192 行 |
WHERE id BETWEEN 1 AND 10000 |
回表 10000 次,极慢 | 连续读几个 granule,极快 |
| 写入维护成本 | 高(插入时更新 B-Tree) | 低(追加) |
核心结论 :稀疏索引是 ClickHouse 为海量数据扫描和范围查询做的刻意取舍------牺牲点查精度,换取极低的存储开销和极高的扫描性能。
六、优化稀疏索引的最佳实践
6.1 把高频过滤列放在 ORDER BY 最前面
sql
-- ✅ 好的设计:查询总是带 event_date
ORDER BY (event_date, user_id)
-- ❌ 差的设计:user_id 在第一位,但查询很少用它过滤
ORDER BY (user_id, event_date)
6.2 避免点查,改用范围查询
sql
-- ❌ 点查(需要 granule 内扫描)
WHERE user_id = 12345
-- ✅ 范围查询(连续 granule,性能更好)
WHERE user_id >= 12345 AND user_id < 12346
6.3 合理设置 index_granularity
sql
CREATE TABLE table (
...
) ENGINE = MergeTree()
SETTINGS index_granularity = 16384; -- 增大到 16384,索引更小,但块内扫描更大
index_granularity |
索引大小 | 点查性能 | 范围查询性能 |
|---|---|---|---|
| 8192(默认) | 基准 | 基准 | 基准 |
| 4096 | 2倍 | 更好 | 稍差(更多索引条目) |
| 16384 | 减半 | 更差 | 更好(连续块更大) |
七、常见误区与澄清
| 误区 | 真相 |
|---|---|
| "ClickHouse 没有索引" | ❌ 有稀疏索引,只是和 B-Tree 不同 |
| "索引越大越好" | ❌ 稀疏索引追求小,越小扫描越快 |
| "点查也能很快" | ⚠️ 可以,但需要配合分区裁剪和主键设计,且不如 MySQL |
| "ORDER BY 只用于排序" | ❌ 它还决定了数据的物理顺序和稀疏索引的结构 |
八、总结
| 问题 | 答案 |
|---|---|
| 什么是稀疏索引? | 每隔 8192 行记录一个索引项,定位到数据块而非具体行 |
| 为什么不用 B-Tree? | B-Tree 太大,维护成本高,不适合 OLAP 的海量数据场景 |
| 点查怎么办? | 尽量避免点查;如需点查,配合分区裁剪 + ORDER BY 优化 |
| 索引大小有多少? | 约数据量的 0.1%,10 亿行数据索引仅 ~2MB |
| 什么时候稀疏索引最有效? | 范围查询、时间序列扫描、需要快速跳过大量数据块的场景 |
一句话记住
稀疏索引是一个"跳着看的目录":路标很少,但能帮你快速跳过大量无关数据。它适合 OLAP 的海量扫描,不适合 OLTP 的精确点查。
如需深入了解 ClickHouse 的部署架构选型、分片与副本机制详解、分布式表原理剖析、无中心架构设计哲学、生产环境集群调优、多副本一致性实践、ClickHouse Keeper 核心原理等内容,请持续关注本专栏《ClickHouse 一站式从入门到实战》系列文章。
在 MySQL 中,我们习惯了 B-Tree 索引的"精准打击"------一条查询瞬间定位到行。但在 ClickHouse 里,你会发现索引完全不是一回事:没有 B-Tree,没有行级定位,甚至索引文件小得可以忽略不计。这就是稀疏索引。本文将深入解析稀疏索引的设计原理、工作方式,以及它为什么更适合 OLAP 场景。