ClickHouse 延迟物化深度解析
这篇文章系统拆解了 ClickHouse 延迟物化从 V1 到 V2 的演进路径,核心目标是解释“如何减少无效列读取与过早物化”并量化其收益与代价。演进背景与总体思路先定义延迟物化问题:在过滤前尽量不读取/不展开重列。给出 V1 ColumnLazy 方案与 V2 双分支 QueryPlan 方案的动机差异。明确分析基…
ClickHouse 延迟物化深度解析 本文深入讲解 ClickHouse 延迟物化(Lazy Materialization)的原理与源码实现,从 V1 的 ColumnLazy 机制到 V2 的双分支 QueryPlan 架构。 代码版本:本文基于 ClickHouse v26.1.1.1-new tag 分析。目录引言与背景前置知识V1 实现原理V2 全局执行流程V2 核心组件详解V1 vs V2 vs AST 重写对比调试技巧与常见问题代码索引References1. 引言与背景1.1 什么是延迟物化SELECT * FROM hits ORDER BY EventTime LIMIT 10 如果 hits 表有 200 列、1 亿行数据,传统执行方式会:读取全部 200 列 → 排序 → 丢弃 99.99999% 的行 → 返回 10 行。 延迟物化(Lazy Materialization)的做法不同:只读取排序列(EventTime)+ 行位置信息排序后确定最终需要的 10 行的位置只读取这 10 行的其他 199 列数据 I/O 量从 "200 列 × 1 亿行" 降低到 "1 列 × 1 亿行 + 199 列 × 10 行",节省了 99.5% 的 I/O。1.2 延迟物化的通用性 延迟物化的核心思想是推迟读取不参与当前计算的列,这个思想可以用在很多场景: Top-N 查询:只用排序列参与排序,其他列等 LIMIT 之后再读SELECT * FROM logs ORDER BY timestamp LIMIT 100 WHERE 过滤:只用过滤列判断哪些行需要,其他列等过滤完再读SELECT * FROM events WHERE user_id = 12345 -- 如果只有 0.1% 的行匹配,可节省 99.9% 的 I/O JOIN:只用 JOIN key 做匹配,其他列等 JOIN 完再读SELECT t1.*, t2.name FROM big_table t1 JOIN small_table t2 ON t1.key = t2.key 聚合:只读聚合需要的列,其他列按需读取SELECT category, SUM(amount), details FROM orders GROUP BY category -- details 列可以延迟读取 本文聚焦 ClickHouse 对 ORDER BY + LIMIT 场景的延迟物化实现。这套实现思路(双分支 QueryPlan + 行索引对齐)是通用的,可以推广到上述其他场景。1.3 ClickHouse 延迟物化演进史 ClickHouse 的延迟物化经历了两个版本: | 版本 | 函数名 | PR/Issue | 时间 | 状态 | |------|--------|----------|------|------| | V1 | optimizeLazyMaterialization | PR #55518 | 2023-10 创建,2025-03 合入 | 已废弃 | | V2 | optimizeLazyMaterialization2 | PR #90309 | 2025-11 创建,2025-12 合入 | 当前使用 | V1 的问题(参见 Issue #79645):-- V1 性能测试(hits 数据集,热查询) SELECT * FROM hits ORDER BY EventTime LIMIT 100; -- V1 (ColumnLazy): 4.516 sec -- 手动 AST 重写 SELECT * FROM hits WHERE (_part_starting_offset + _part_offset) IN (SELECT _part_starting_offset + _part_offset FROM hits ORDER BY EventTime LIMIT 100) ORDER BY EventTime; -- AST 重写: 0.181 sec(快 25 倍) LIMIT 越大,V1 的劣势越明显(因为逐行读取的开销线性增长)。 AST 重写为什么这么快?因为 ClickHouse 对 IN subquery 有专门的 CreatingSet 优化机制,可以利用索引跳过不需要的 marks。详见 §6.3 AST 重写的原理。 V2 的改进: | 问题 | V1 实现 | V2 实现 | |------|---------|---------| | 读取粒度 | 逐行读取(每次 1 行) | 批量读取(按 mark/granule) | | 并发支持 | 不支持 | 支持(MergeTreeReadPoo…
正在初始化 WebAssembly 引擎…
首次编译原生模块可能需要数秒
就绪后,页面交互将以接近原生的速度运行