ClickHouse 的 PRIMARY KEY 不是唯一约束——它是 稀疏排序索引,每 granule 一条,配合物理排序做 granule 剪枝。二级 Data Skipping Index 进一步跳过 granule,与 PG B-Tree 指针索引本质不同。
一、稀疏主键
primary.idx:granule \(i\) 存该行 ORDER BY
前缀键值。
查询
WHERE date >= '2024-01-01' AND date <= '2024-01-31'
→ 二分 primary.idx → Mark Range。
不能保证 WHERE id = 1
只读一行——可能读整个 granule(最多
index_granularity 行)。
flowchart TB
PK[primary.idx 每 granule 1 键]
PK --> R[Mark Range]
R --> READ[读 granule 内所有行再 filter]
二、ORDER BY 与 PRIMARY KEY
PRIMARY KEY 必须是 ORDER BY
前缀;可 ORDER BY (a,b,c) PRIMARY KEY (a,b)
减少索引体积。
排序键选择决定 merge 归并顺序与剪枝效果——DDL 核心设计。
三、Data Skipping Index
ALTER TABLE t ADD INDEX idx_url url TYPE bloom_filter GRANULARITY 4;| 类型 | 机制 |
|---|---|
| minmax | granule 内 min/max |
| set | 值集合(有大小限制) |
| bloom_filter | 等值探测 |
| tokenbf_v1 | 分词 bloom |
GRANULARITY n:每 \(n\) 个 granule 一条索引项。文件
skp_idx_* .idx + .mrk2。
flowchart TB
Q[WHERE url = ?] --> PK[primary.idx 剪枝]
PK --> SK[bloom_filter 跳数索引]
SK --> G1[Granule 跳过]
SK --> G2[Granule 保留 → 读列]
G2 --> MRK[Mark → .bin]
四、与 PG B-Tree 对照
| PG B-Tree | CH 稀疏 PK + Skip | |
|---|---|---|
| 粒度 | 行/页 | granule |
| 唯一 | 可 UNIQUE | 不唯一 |
| 二级 | 任意列 B-Tree | 跳数索引,非精确 |
| 维护 | 每行更新 | insert/merge 批量写 |
五、设计建议
- PK 前缀放 高频范围过滤 列(日期、租户 id)
- 高基数等值辅助 bloom;低基数 set/minmax
- 错误索引几乎无剪枝还占空间
六、EXPLAIN indexes=1
与 第 5 篇 联用,查看 Skip 索引排除 granule 数。本环境无实例,不贴输出。
七、学术谱系:稀疏主键 · Zone Map · Skip Index
| 阶段 | 概念 / 文献坐标 | 本篇落点 |
|---|---|---|
| 行存 | B-Tree 定位行(PG 14-btree) | 对照:CH PK 不定位行 |
| 列存经典 | Zone map / min-max 段摘要 | minmax 跳数索引;PK 稀疏边界 |
| C-Store | 排序投影利于范围 | ORDER BY / PRIMARY KEY 前缀 |
| CH | set / bloom skip index | 辅助剪枝,非第二套 B-Tree |
争论:多建 skip index 换剪枝 vs 索引维护与空间。错误索引「几乎无剪枝还占空间」(第五节)——与 Abadi「系统栈要完整」一致:索引必须服务真实谓词。
7.1 工程间隙
| 论文 / 教科书 | ClickHouse |
|---|---|
| 理想均匀分区 | 时间歪斜导致 PK 前缀失效 |
| Projection 作主加速手段 | CH 主路径仍是排序键 + skip;PROJECTION
可选(第 01 篇开放问题) |
7.2 开放问题
- Projection 与 skip index 职责如何划分?
可检验:同查询只加 projection vs 只加 bloom 的
EXPLAIN indexes与 parts 体积。 - 自适应跳数索引(按查询历史推荐)——研究与产品都在演进,无统一标准。
八、小结
索引 = 减少 granule,不是 定位行。理解这一点可避免把 ClickHouse 当 OLTP 主库。
上一篇:Merge 与 Mutation
参考资料
核心论文
- Stonebraker et al., C-Store, VLDB 2005(排序投影;A 级)。
- Abadi et al., FnT DB 2013 综述中的 zone-map / 段摘要坐标(A 级)。
规范 / 源码 / 文档
- ClickHouse Documentation, Primary Keys、Data Skipping Indexes(A 级)。
- PostgreSQL 系列, B-Tree(对照)。
稀疏主键 vs B-Tree
PG B-Tree 叶项指向 heap tuple(14-btree);CH
primary.idx 每 granule
一条,不指向行,只用于 granule
范围剪枝。
跳数索引类型
| 类型 | 存储 | 适用 |
|---|---|---|
| minmax | 每 granule min/max | 范围谓词 |
| set | granule 内值集合(有上限) | IN 列表 |
| bloom_filter | Bloom | 高基数等值 |
| tokenbf_v1 / ngrambf_v1 | 文本 token | LIKE 辅助 |
定义在
INDEX idx_name expr TYPE ... GRANULARITY g;GRANULARITY
表示每多少个 granule 写一条索引项。
Mark Range 算法(概念)
对每个 Part:
- 用
primary.idx二分/扫描,找与 PK 谓词相交的 granule 下标区间 \([g_{lo}, g_{hi}]\)。 - 跳数索引进一步缩小区间(第 7 篇)。
- 对每列 Mark 文件取 \([g_{lo}, g_{hi}]\) 对应压缩块并解压。
- PREWHERE 列优先读,结果位图过滤后再读其余列(若优化器下推)。
PREWHERE 语义
PREWHERE
不是语法糖:优化器将选择性高的条件移到存储层,减少列
IO。官方建议高选择性条件放 PREWHERE;WHERE 其余
predicate 在内存 Block 上执行。
并行
- Part 级:
MergeTreeReadPool分配 Part 给线程。 - Granule 级:Mark Range 切分。
max_threads与max_streams_to_max_threads_ratio共同约束。
读完这篇,下一步读什么
优先读同系列或同问题的下一篇,把单篇消费变成主题集群。
【列存引擎内核】列存基础与 ClickHouse 架构
行存 vs 列存的带宽、压缩与向量化三角;ClickHouse Server 进程模型、线程池与 MergeTree 引擎家族地图;src/Storages 与 src/Processors 源码入口。对照 PG 行存与 LSM 写优化路径,版本锚定 ClickHouse 24.x LTS。
【列存引擎内核】MergeTree Part 文件格式
ClickHouse MergeTree Part 目录结构:columns.txt、checksums.txt、.bin、.mrk2、primary.idx 语义,Granule 与 Mark 的定位作用,Wide/Compact 布局与 MergeTreeDataPart 源码入口。版本锚定 24.x LTS。
【列存引擎内核】压缩与编码
ClickHouse 列压缩:LZ4、ZSTD、Delta、DoubleDelta、Gorilla 时序编码与列类型关系;CODEC 链顺序、LowCardinality 与 PG TOAST 对照。压缩比须本机实测,本文不编造倍数。
【列存引擎内核】向量化执行引擎
ClickHouse Block 列向量 batch、IProcessor Pipeline 与 filter/project/aggregate 向量实现;对照 PostgreSQL 火山模型 ExecProcNode。源码入口 src/Processors、src/Columns。24.x LTS。