分区、索引、物化视图与 Profile 排查
一条主线:把"慢"拆成可观测的物理代价,再逐层削减
分区裁剪 → 索引跳数 → 物化视图改写 → Profile 归因 → 参数与建模闭环
摘要
ClickHouse 与 Apache Doris 都是列式 MPP 分析引擎,但"查询慢"的成因分布并不相同。本文不按"分区—索引—物化视图—Profile"做并列罗列,而是确立一条贯穿全文的分析主线:任何慢查询都可以被还原为"扫描了多少数据、跳过了多少数据、计算了多少行、等待了多少资源"四个可观测物理量,优化即是对这四个量的逐层削减。围绕这条主线,文章依次讨论分区裁剪的边界条件与失效场景、两级索引(稀疏主键索引与跳数索引 / 前缀索引与 ZoneMap)的命中判定、物化视图的透明改写与一致性代价,以及如何用 Profile、Query Log、火焰图把耗时定位到具体算子。全文给出可执行的排查步骤、参数清单与选型决策表,并对向量化执行、CBO 与 AI 辅助诊断的前沿方向做出研判。
目录
一、问题的重新定义:慢查询的四个物理量
多数团队处理慢查询的方式是"看到慢就加索引、加分区、加物化视图",这是一种经验驱动的试错。笔者认为更可靠的做法是先建立统一的度量语言。在列式 MPP 引擎中,一条查询的墙钟时间 T 大致可分解为:
T ≈ T_scan(读取字节数 / 有效带宽) + T_compute(处理行数 × 单行算子代价) + T_shuffle(网络传输字节数 / 链路带宽) + T_wait(排队、锁、内存溢出落盘、调度)
这四个分量分别对应四类优化手段:分区裁剪削减 T_scan 的输入规模;索引削减 T_scan 中"读了但没用"的部分;物化视图同时削减 T_scan 与 T_compute;Profile 则用于判断当前瓶颈落在哪一项。这条主线的好处是:任何一次优化动作,都必须能回答"我削减了哪个物理量、削减了多少",否则就是盲改。
1.1 为什么"慢"必须先量化再动手
ClickHouse 官方文档在查询优化章节中反复强调一个观点:先看 system.query_log 中的 read_rows、read_bytes、memory_usage 与 query_duration_ms 的比值关系,再决定优化方向。本文评述:这一建议的价值不在于"看日志"本身,而在于它隐含了一个判据——如果 read_rows 远大于结果行数,问题在裁剪与过滤;如果 read_rows 正常但耗时高,问题在计算或等待。这个二分法应当成为所有排障的第一步。
Doris 侧对应的观测入口是 information_schema 下的审计表与 FE 的审计日志插件,以及 SHOW QUERY PROFILE。Doris 的 Profile 以树形结构呈现每个算子(Operator)的耗时、行数、内存与等待时间,粒度比 ClickHouse 的文本级 trace 更接近执行计划树。笔者认为,两个引擎的观测哲学差异值得注意:ClickHouse 更依赖"事后日志 + 采样 trace",Doris 更倾向"执行期 Profile 树",这直接影响了排障路径的设计(详见第五章)。
1.2 一个可复用的量化模板
上表阈值为笔者在多个中等规模集群(数十节点、PB 级以下)中的经验归纳,属于工程经验值而非官方标准,读者应结合自身数据分布重新标定。需要强调的是,这些比值比绝对耗时更稳定,因为它们剥离了硬件与并发波动的影响。
二、分区设计:裁剪的收益边界与失效场景
分区(Partition)在 ClickHouse 与 Doris 中都是"物理目录级"的粗粒度裁剪手段。ClickHouse 的分区键一经定义,分区即对应磁盘上的独立目录,裁剪发生在读取阶段之前;Doris 的分区则与 Tablet 分布绑定,裁剪同时影响扫描与调度。理解这一差异,是判断"分区到底有没有生效"的前提。
2.1 分区裁剪生效的三个必要条件
裁剪不是"建了分区就自动生效"。笔者认为至少需要同时满足三个条件:其一,谓词必须直接作用于分区键列,且不能被函数包裹(如 toDate(ts) = '2024-01-01' 在部分版本中会阻断裁剪,应改写为范围谓词);其二,谓词必须是常量或可在规划期求值的表达式,子查询、参数化视图中的变量在某些版本中会导致裁剪退化;其三,分区粒度要与查询模式匹配——按天分区但查询总是跨月,裁剪收益会被分区数量膨胀带来的元数据开销抵消。
本文评述:很多"分区优化无效"的案例,根因不是分区设计错,而是谓词写法让优化器无法在规划期确定分区范围。因此排查顺序应是"先看 EXPLAIN 的分区裁剪结果,再谈分区键是否合理",而不是反过来。
ClickHouse 可通过 EXPLAIN indexes = 1 观察分区与主键索引的裁剪情况;Doris 可通过 EXPLAIN 输出中的 partitions 字段确认实际扫描分区数。这两个命令应当成为每次慢查询排查的固定动作。
2.2 分区键选择的工程判据
分区键的本质是"用元数据换扫描量"。分区越多,裁剪越精细,但元数据、文件句柄、合并压力也越大。ClickHouse 官方建议单表分区数量控制在千级以内(来源:ClickHouse 官方文档 "Merge Tree" 章节关于分区数量的说明),Doris 则建议单分区数据量在 1GB 以上以避免小文件问题。笔者在实践中总结出一个粗略判据:
- 时间序列数据:按天或按月分区,取决于查询的典型时间跨度与保留周期;
- 多租户数据:租户数少(< 100)可按租户分区,租户数多则用租户列做前缀索引而非分区;
- 枚举维度:分区基数超过数百时,分区带来的元数据开销往往超过收益,应改用跳数索引或物化视图。
这里有一个常被忽视的反直觉结论:分区并不总是越多越好,过度分区会让"分区裁剪"本身变成瓶颈。当单表分区数达到数万级时,规划期遍历分区元数据的时间可能超过实际扫描时间,这在 ClickHouse 的 system.parts 表膨胀时尤为明显。Doris 侧则表现为 FE 元数据压力与 Tablet 数量膨胀。
2.3 失效场景清单
关于分区裁剪的学术侧进展,可以关注近年 VLDB、SIGMOD 上关于 "partition pruning under parameterized queries" 的讨论,其核心问题正是"运行时参数导致规划期无法裁剪",这与工程中参数化视图的痛点高度一致。延伸阅读可参考 ClickHouse 官方文档的 Partitioning Key 章节 与 Doris 官方文档的 数据分区指南。
三、索引体系:稀疏索引、跳数索引与前缀索引的命中逻辑
分区解决"读哪些目录",索引解决"目录内读哪些块"。两个引擎的索引哲学差异明显:ClickHouse 以稀疏主键索引(sparse primary index)+ 跳数索引(data skipping index)为核心,Doris 以排序键前缀索引 + ZoneMap + 倒排索引为核心。理解"索引为什么没命中",比"加什么索引"更重要。
3.1 ClickHouse:稀疏索引的粒度与代价
ClickHouse 的主键索引不是逐行索引,而是每 8192 行(由 index_granularity 控制)记录一个标记(mark)。这意味着索引只能把扫描范围缩小到"颗粒"级别,颗粒内的行仍需逐行过滤。本文评述:这决定了 ClickHouse 的索引收益上限——它擅长"排除大量无关颗粒",不擅长"精确定位单行"。因此点查场景下,主键顺序与查询谓词前缀是否对齐,比索引本身是否存在更关键。
跳数索引(minmax、set、bloom_filter、ngrambf_v1 等)则是在颗粒之上再加一层"跳过"判断。以 bloom_filter 为例,它适合高基数列的等值过滤,但对范围查询无效;minmax 适合与排序键相关性强的列,但对随机分布列几乎无收益。笔者见过不少团队给低基数列建 bloom_filter,结果索引体积膨胀而命中率极低——这是典型的"索引选型与数据分布不匹配"。
-- 观察跳数索引是否生效(示意) EXPLAIN indexes = 1 SELECT count() FROM events WHERE event_type = 'click' AND user_id = 12345; -- 输出中关注 Granules 的 Selected / Total 比例 -- 若 Selected 接近 Total,说明索引未命中
3.2 Doris:前缀索引与 ZoneMap 的协同
Doris 的 Aggregate/Unique/Duplicate 模型均基于排序键构建前缀索引,其规则是"取排序键前 36 字节"(来源:Doris 官方文档 "前缀索引" 章节)。这意味着排序键的列顺序直接决定索引覆盖范围:把高基数列放在前面,会让后续列无法进入前缀索引。笔者认为,Doris 建模时最常见的错误就是"按业务直觉排列列顺序",而非"按查询谓词频率排列"。
ZoneMap 索引则记录每个数据块中列的 min/max,对范围过滤与等值过滤都有一定效果,尤其适合与排序键弱相关的列。Doris 1.2 之后引入的倒排索引(Inverted Index)进一步补齐了文本与高基数等值查询的能力,其原理接近 Lucene 的倒排结构,对 LIKE、MATCH 类查询收益明显。相关设计可参考 Doris 官方 倒排索引文档。
3.3 索引选型决策表
需要提醒的是,跳数索引并非免费:每个索引都会增加写入放大与存储开销,且只在"过滤后颗粒数显著减少"时才有净收益。笔者建议的验证方法是:在真实数据上对比"建索引前后 read_rows 与耗时",若 read_rows 下降不足 30%,应删除该索引。这一判据来自工程实践,属于经验法则而非官方结论。
四、物化视图:透明改写、聚合态与一致性代价
物化视图(Materialized View,MV)是把"重复计算"提前到写入侧的手段,理论上同时削减 T_scan 与 T_compute。但它是四类手段中最容易"建了却没用上"的一类,原因在于改写(rewrite)是否发生、聚合态是否匹配、以及一致性代价是否可接受。
4.1 ClickHouse 的 MV:写入触发器而非查询改写器
ClickHouse 的物化视图本质是一个"插入触发器":向源表写入时,MV 按定义的目标表结构做一次转换并写入目标表。它不做查询期的透明改写,查询必须显式指向目标表(或通过分布式表/视图封装)。本文评述:这一设计常被误解为"缺陷",实则是明确的取舍——它把改写责任交给用户,换来了可预测的执行路径。因此 ClickHouse 场景下,"MV 没用上"往往不是优化器的问题,而是查询根本没指向 MV 目标表。
ClickHouse 的 AggregatingMergeTree 配合 -State / -Merge 组合函数,是构建预聚合 MV 的标准范式。其代价是:查询侧必须使用对应的 Merge 函数,否则会读到中间态。这一点在跨团队协作中极易出错,建议在表注释与查询模板中显式标注。
4.2 Doris 的 MV:异步物化与透明改写
Doris 的异步物化视图支持查询透明改写(transparent rewrite),优化器会在规划期判断某个 MV 是否能覆盖当前查询。这带来便利,也带来"改写未命中"的排查需求。常见未命中原因包括:聚合粒度不一致、谓词无法从 MV 推导、Join 关系缺失、以及 MV 数据未刷新(staleness)。Doris 提供 EXPLAIN 输出中的 rewrite 信息用于确认。可参考 Doris 官方 物化视图文档。
笔者认为:物化视图的核心矛盾是"预计算收益"与"一致性/存储成本"的权衡。对 T+1 报表类场景,异步 MV 的收益极高;对要求强一致的实时看板,MV 的刷新延迟可能反而制造新的问题。选型时应先问"业务能容忍多久的延迟",再问"能省多少扫描"。
4.3 一致性代价的量化思路
MV 的存储成本约等于"目标表行数 × 列宽",刷新成本约等于"一次等价查询的计算量"。若源表日增 1 亿行、MV 聚合后日增 100 万行,则存储节省约两个数量级,刷新成本可控;但若聚合维度基数极高(如按用户 ID 聚合),MV 行数可能与源表同量级,此时 MV 的净收益为负。这一判断应基于真实基数统计,而非拍脑袋。
五、Profile 排查:从 Query Log 到算子级火焰图
前三章是"削减输入",本章是"定位瓶颈"。Profile 的价值在于把墙钟时间拆到算子级,回答"时间到底花在哪"。两个引擎的 Profile 能力差异较大,排查路径也应不同。
5.1 ClickHouse:query_log + trace + 采样
ClickHouse 的 system.query_log 记录了每次查询的耗时、读写行数、内存峰值、异常信息,是排障的第一入口。更细的算子级信息可通过 system.trace_log(需开启采样)与 EXPLAIN PIPELINE 获取。本文评述:ClickHouse 的 Profile 更偏"事后归因",适合复盘;实时定位则依赖 clickhouse-client --progress 与 system.processes 的实时观测。
-- 找出最近最慢的 10 条查询(示意)
SELECT query_id, query_duration_ms, read_rows, read_bytes,
memory_usage, query
FROM system.query_log
WHERE type = 'QueryFinish'
ORDER BY query_duration_ms DESC
LIMIT 10;
5.2 Doris:Profile 树与算子级指标
Doris 的 SHOW QUERY PROFILE 输出一棵算子树,每个节点包含 ExecTime、RowsReturned、PeakMemory、WaitTime 等指标。排查时应自顶向下找"耗时占比最高"的子树,再判断是扫描慢、计算慢还是等待慢。笔者认为,Doris Profile 中最容易被忽视的是 WaitTime——它揭示了排队与资源争抢,而这往往是"查询本身不慢但就是慢"的真凶。
5.3 火焰图与采样分析
对 CPU 密集型慢查询,火焰图(flame graph)能直观展示热点函数。ClickHouse 可通过 perf 采样结合 clickhouse-obfuscator 符号化生成;Doris 侧可借助 BE 的 perf 采样与 pprof 接口。延伸阅读可参考 Brendan Gregg 的火焰图方法,这是跨引擎通用的分析工具。
需要强调的是,Profile 只回答"哪里慢",不回答"为什么慢"。把 Profile 结论映射回前四章的物理量,才是完整闭环:例如 Profile 显示某聚合算子耗时占比 70%,应回到"是否可用 MV 预聚合""是否可减少 Group By 基数""是否内存不足导致落盘"三个方向逐一验证。
六、参数、并发与资源:被忽视的第二战场
很多"优化了 SQL 还是慢"的案例,根因不在 SQL 而在资源竞争。列式引擎的并行度、内存上限、落盘策略、并发队列,都会显著影响墙钟时间。
6.1 并行度与内存的跷跷板
ClickHouse 的 max_threads 与 Doris 的并行度(parallel_pipeline_task_num 等)都遵循同一规律:并行度提升会放大内存峰值,内存不足则触发落盘(spill),落盘又拖慢查询,形成负反馈。本文评述:调参的正确顺序是先压内存峰值(减少扫描列、缩小聚合基数),再谈并行度,否则只是把瓶颈从 CPU 搬到磁盘。
6.2 并发与排队
高并发场景下,查询排队时间可能远超执行时间。ClickHouse 可通过 max_concurrent_queries 与资源池控制;Doris 提供 Workload Group 做资源隔离与排队管理。笔者建议对"慢查询"做分层:交互式查询与批处理查询走不同资源池,避免批任务把交互查询拖慢。这一策略在 Doris 官方 Workload Group 文档 中有详细说明。
6.3 常见参数速查
七、建模与写入侧:慢查询的上游根因
查询慢有时是"写入侧埋的雷"。小文件过多、分区过碎、排序键与查询不匹配、宽表过度冗余,都会在查询期集中爆发。笔者认为,建模阶段的决策对查询性能的影响,往往大于查询期的任何调优。
7.1 小文件与合并压力
ClickHouse 的 part 数量过多会拖慢扫描与合并;Doris 的 Tablet 过多会拖慢 FE 调度。两者都要求写入侧做批量攒批(batch),避免高频小写入。工程上常见的做法是引入缓冲层(如 Kafka + 定时导入),把写入频率控制在分钟级。
7.2 排序键与查询模式的匹配
排序键决定了数据在磁盘上的物理顺序,也决定了前缀索引与 minmax 索引的有效性。一个实用的方法是:统计 Top 20 慢查询的 WHERE 子句列组合,取出现频率最高的前缀作为排序键。这一方法在 Doris 与 ClickHouse 中通用,属于"数据驱动建模"的基本功。
八、决策表与端到端排查 SOP
把前七章压缩成一张可执行的 SOP,是本文的落地目标。以下流程按"先量化、再定位、后优化"的顺序组织,每一步都有明确的判据与退出条件。
这套 SOP 的核心是"每一步都有退出条件",避免陷入"无限调优"。笔者建议团队把它固化为排障模板,并在每次慢查询复盘时记录"命中了哪一步、削减了哪个物理量、效果如何",长期积累后即可形成组织级的性能知识库。
九、前沿研判:CBO、向量化与 AI 辅助诊断
从学术与工业进展看,查询优化正在从"规则驱动"走向"代价驱动 + 学习驱动"。Doris 已引入基于代价的优化器(CBO)与统计信息收集机制,ClickHouse 也在持续增强其查询规划能力。本文评述:CBO 的收益高度依赖统计信息的质量,而统计信息的维护成本又常被低估,这是未来几年工程侧的主要矛盾之一。
向量化执行(vectorized execution)已是两个引擎的标配,其进一步演进方向是"编译执行 + 自适应批大小"。近年数据库顶会中关于 "adaptive query processing" 与 "learned cost model" 的讨论,为自动调参提供了理论可能,但距离生产可用仍有距离。笔者认为,短期内更现实的进展是"AI 辅助诊断"——用 LLM 对 Profile 与 query_log 做归因建议,把专家经验产品化。这一方向已有开源尝试,但需警惕"建议看似合理实则错误"的风险,必须保留人工复核环节。
对读者而言,值得持续跟踪的资源包括:ClickHouse 官方博客的 性能专题、Doris 社区的 技术博客,以及 VLDB/SIGMOD 中关于列式引擎与查询优化的论文。视频方面,ClickHouse 与 Doris 的官方 Meetup 录像通常包含大量真实调优案例,适合作为工程参考。
十、结语与主要参考文献
回到主线:慢查询优化不是"堆手段",而是"削减物理量"。分区削减扫描输入,索引削减无效读取,物化视图削减重复计算,Profile 负责归因,参数与建模负责兜底。四者构成一个闭环,缺一环就会退化为经验试错。笔者认为,真正拉开工程师差距的,不是记住多少参数,而是能否在动手前把"慢"翻译成可度量的物理代价。
主要参考文献(节选)
- ClickHouse Documentation. "MergeTree" & "Partitioning Key". 2024. https://clickhouse.com/docs
- ClickHouse Documentation. "Data Skipping Indexes". 2024. https://clickhouse.com/docs/en/engines/table-engines/mergetree-family/mergetree
- Apache Doris Documentation. "Data Partitioning". 2024. https://doris.apache.org/docs/table-design/data-partitioning/
- Apache Doris Documentation. "Materialized View Overview". 2024. https://doris.apache.org/docs/query-acceleration/materialized-view/overview/
- Apache Doris Documentation. "Workload Group". 2024. https://doris.apache.org/docs/admin-manual/workload-management/workload-group/
- B. Gregg. "The Flame Graph". Communications of the ACM, 2016.
- M. Stonebraker et al. "C-Store: A Column-oriented DBMS". VLDB 2005.
- Apache Doris Community Blog. "Query Acceleration Practices". 2023-2024. https://doris.apache.org/blog
说明:本文引用的官方文档与博客均以访问时的公开版本为准;文中经验阈值与判据为工程归纳,非官方标准,读者应结合自身环境验证。涉及性能对比的数据均来自公开文档或笔者模拟整合,未虚构实验。
本文内容仅为作者学习、思考、经验、笔记的总结,仅供技术交流与参考。文中观点仅代表笔者个人思辨,不构成任何学术建议、商业建议或专业建议。所有数据来源已标注,引用时请以原始文献为准。

