SELECT 在 MergeTree
上不是「全表扫磁盘」——primary.idx
与跳数索引先定 Mark Range,再按列读
.bin 压缩块,PREWHERE
可能只读过滤列。本文串起规划器下推到存储的边界,以及 Part /
线程并行。
本环境未安装
ClickHouse,EXPLAIN 示例仅给 SQL
框架,不伪造计划文本。
一、读路径总览
flowchart TB
SQL[SQL] --> OPT[Optimizer]
OPT --> PK[PK granule 剪枝]
OPT --> SKIP[跳数索引剪枝]
PK --> MR[Mark Range]
SKIP --> MR
MR --> PRE[PREWHERE 列读]
PRE --> REST[其余投影列]
REST --> BLK[Block → Pipeline]
二、Mark Range
对每个 Active Part:
- 读取内存中
primary.idx(或缓存)。 - 按 PK 谓词二分得 granule 下标区间 \([g_{lo}, g_hi}]\)。
- 列 Mark
.mrk2同区间映射到.bin块。
多 Part 各自 Mark Range,结果在算子层合并(UNION ALL 语义)。
三、PREWHERE vs WHERE
| PREWHERE | WHERE | |
|---|---|---|
| 执行位置 | 存储层优先 | 通常 Block 上 |
| 目的 | 少读列 | 完整 predicate |
| 写法 | PREWHERE 子句或优化器自动 |
WHERE |
官方建议:高选择性条件利于 PREWHERE(如
user_id = 123 在大表)。
实验方法(本机):对比 system.query_log 的
read_rows、read_bytes 与是否启用
PREWHERE——须自测填数。
四、并行读
- Part 级:线程池每 Part 或 Part 子集。
- Granule 级:大 Part 内切分 Mark Range。
max_threads、max_streams_to_max_threads_ratio、use_uncompressed_cache影响行为。
flowchart LR
Q[Query threads] --> P1[Part A Mark Ranges]
Q --> P2[Part B Mark Ranges]
Q --> P3[Part C Mark Ranges]
P1 --> BLK1[Block]
P2 --> BLK2[Block]
P3 --> BLK3[Block]
BLK1 --> MERGE[算子层合并]
BLK2 --> MERGE
BLK3 --> MERGE
五、EXPLAIN indexes=1
EXPLAIN indexes = 1
SELECT count()
FROM hits
WHERE EventDate = '2014-03-01' AND UserID = 123;用于查看 每个 Part 的 granule 剪枝 与跳数索引使用情况。解读要点:
Parts/Granules总数 vs 选中数PrimaryKey条件如何缩 rangeSkip索引名与Granules排除数
请在本机执行后阅读输出;本文不粘贴虚构计划。
六、与 LSM 读放大对照
LSM 多层 SST 叠加(LSM 概览);MergeTree 多 Part 叠加 + 稀疏索引。merge 降低 Part 数 = 降低读放大(第 6 篇)。
七、学术谱系:Late Materialization · PREWHERE · Mark Range
| 阶段 | 文献 / 机制 | 本篇落点 |
|---|---|---|
| 2008 | Abadi et al., SIGMOD — late materialization 等 | 先过滤再取宽列;减少中间物化 |
| CH | PREWHERE |
在读路径早期用部分列裁剪 granule/行 |
| CH | Mark Range + 稀疏 PK | 先定 哪些 granule,再解压(第 2、7 篇) |
| 对照 | 行存 volcano 早物化整行 | PG 执行器 |
谱系结论:Abadi 2008 的「列存优势清单」在 CH 上落成 PREWHERE + 列裁剪 + 向量 Block;Mark 文件是论文少写的 工程索引,把「逻辑行号」接到压缩块偏移。
7.1 工程间隙
| 论文策略 | ClickHouse 现实 |
|---|---|
| 优化器自动选 late mat | 需合理 PREWHERE / 主键前缀;误用收益小 |
| 理想连续 granule | 多 Part、差排序键 → 几乎全表 granule |
| 忽略解压 | 压缩块解压常大于谓词 CPU(第 3 篇) |
7.2 开放问题
- PREWHERE 自动改写是否应更激进? 可检验:同查询手写 vs 自动 PREWHERE 的读字节(入口:Abadi SIGMOD’08;CH PREWHERE)。
- 自适应 Mark 与固定 granularity 对剪枝稳定性的影响——与第 2 篇开放问题联动。
八、小结
读路径 = 索引定 granule → Mark 定块 → 解压列 → PREWHERE 裁剪 → Block 进 pipeline。
上一篇:向量化执行
下一篇:Merge 与 Mutation
参考资料
核心论文
- Abadi, Madden & Hachem, Column-Stores vs. Row-Stores: How Different Are They Really?, SIGMOD 2008(late materialization 等;A 级)。
- Boncz et al., MonetDB/X100, CIDR 2005(向量批与读路径衔接;A 级)。
规范 / 源码 / 文档
- ClickHouse Documentation, PREWHERE、EXPLAIN(A 级)。
- ClickHouse Source,
MergeTreeSelectProcessor、MergeTreeReadPool(A 级)。
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。