ClickHouse v22.4.3.3-stable 补丁解析:JOIN 与 COLUMNS 列匹配器的空指针解引用缺陷修复
【免费下载链接】ClickHouseClickHouse® is a real-time analytics database management system项目地址: https://gitcode.com/GitHub_Trending/cli/ClickHouse
本篇文章聚焦 ClickHouse 官方补丁版本 v22.4.3.3-stable 中的一项关键 Bug 修复:在 JOIN 查询中结合COLUMNS(...)列匹配器使用时可能触发的空指针解引用(nullptr dereference)。文章将结合 changelog 归档文档、解析器与查询改写器源码以及 stateless 测试用例,从缺陷表象、触发机制、底层实现到验证方法进行全链路拆解,帮助读者理解 COLUMNS 匹配器在复杂 JOIN 场景下的行为边界,并掌握如何复现与验证该修复。
一、版本背景:一次面向稳定版的紧急修补
在 v22.4.3.3-stable 变更日志中,该版本被标记为"Bug Fix (user-visible misbehaviour in official stable release)",即官方稳定版中用户可见的错误行为修复。其唯一一条变更记录如下:
Backported in #36582:修复 JOIN 与 COLUMNS matcher 中的空指针解引用,此修复解决了 #36416 问题,对应 PR #36417,由 Amos Bird 提交(#36430)。
这里包含几个关键信息:
- 修复性质:空指针解引用(nullptr dereference),属于会导致服务端崩溃(segfault)或未定义行为的严重缺陷;
- 影响范围:同时涉及两条查询路径——
JOIN(多表连接)与COLUMNS列匹配器(column matcher); - 版本关系:该修复作为 backport 进入 v22.4.3.3-stable,说明其同时在主开发分支(对应 #36430)与稳定分支(对应 #36582)中被合入;
- 修复作者:Amos Bird,ClickHouse 社区核心贡献者之一。
从版本命名规则看,v22.4.3.3-stable 属于 v22.4 系列的第三轮补丁(.3),其比较基线为 v22.4.2.1-stable,说明这是针对稳定分支的快速迭代修补,用户升级到该版本即可规避此缺陷。
二、缺陷主角:COLUMNS 列匹配器是什么
要理解该缺陷,首先需要明确COLUMNS(...)在 ClickHouse SQL 中的地位。它不是普通函数,而是一种列匹配器(column matcher),允许在SELECT列表中按正则表达式或显式列名批量展开列。在解析器实现中,共定义了四类匹配器 AST 节点:
| AST 节点类型 | SQL 形态 | 匹配规则 |
|---|---|---|
ASTColumnsRegexpMatcher | COLUMNS('pattern') | 按正则表达式匹配列名 |
ASTColumnsListMatcher | COLUMNS(a, b, c) | 匹配显式列名列表 |
ASTQualifiedColumnsRegexpMatcher | table.COLUMNS('pattern') | 限定表名 + 正则匹配 |
ASTQualifiedColumnsListMatcher | table.COLUMNS(a, b) | 限定表名 + 显式列名 |
从 ASTColumnsMatcher.cpp 的appendColumnName与formatImpl实现可以看到,正则型匹配器(ASTColumnsRegexpMatcher)内部持有pattern字符串,并且同时支持两种呈现形式:
// 常规形式 ostr << "COLUMNS" << "(" << quoteString(pattern) << ")"; // 兼容 * LIKE 的形式(format_as_asterisk_like 为真时) ostr << "* " << (asterisk_like_case_insensitive ? "ILIKE " : "LIKE ") << quoteString(asterisk_like_pattern);这说明 ClickHouse 在后续演进中还为* LIKE pattern语法提供了统一的 AST 表示,而updateTreeHashImpl将pattern纳入 SipHash 计算,确保不同写法但语义相同的查询在查询缓存中保持一致。
在反序列化路径(ASTColumnsMatcher.cpp#L287-L353)中可以看到,ASTColumnsListMatcher的column_list成员被强制要求只能包含标识符(ASTIdentifier),否则抛出BAD_ARGUMENTS异常——这是保证匹配器语义安全的关键校验。
与列转换器(Transformer)的组合
COLUMNS(...)通常与APPLY、EXCEPT、REPLACE等列转换器(columns transformer)联用,例如:
SELECT COLUMNS('[jk]') APPLY(toString) APPLY(length) FROM columns_transformers; SELECT COLUMNS(i, j, k) APPLY(quantiles(0.5)) FROM columns_transformers;在 01470_columns_transformers.sql 测试中,详细覆盖了以下行为:
* APPLY(sum):对全部列应用聚合函数;columns_transformers.* EXCEPT(j) APPLY(avg):限定表 + 排除列 + 应用函数;EXCEPT STRICT i:严格模式,若列不存在则报错(NO_SUCH_COLUMN_IN_TABLE);- 多个
APPLY/REPLACE串联时的语义顺序(REPLACE在APPLY之后将无法再匹配到列)。
从源码结构看,ASTColumnsMatcher系列节点均持有transformers成员(类型为ASTColumnsTransformerList),展开匹配的列后逐个套用转换器,这解释了测试中"EXCEPT在APPLY之后匹配不到任何列"的语义。
三、缺陷触发场景:JOIN 与 COLUMNS 的碰撞
空指针解引用往往发生在"匹配器需要解析为具体列,但解析上下文不完整"的路径上。最典型的场景正是在 JOIN 查询中使用 COLUMNS 匹配器,此时 ClickHouse 的查询改写器需要提前将匹配器展开为具体的列表达式列表。
01839_join_to_subqueries_rewriter_columns_matcher.sql 给出了该场景的回归测试样例:
-- 基础场景:三个表的 LEFT JOIN,列名存在冲突(test 列在三个表中同名) SELECT (if(a.test == 'a', b.test, c.test)) AS `a.test` FROM (SELECT 1 AS id, 'a' AS test) a LEFT JOIN (SELECT 1 AS id, 'b' AS test) b ON b.id = a.id LEFT JOIN (SELECT 1 AS id, 'c' AS test) c ON c.id = a.id; -- 缺陷场景:在 JOIN 中直接使用 COLUMNS 匹配器 SELECT COLUMNS('test') FROM (SELECT 1 AS id, 'a' AS test) a LEFT JOIN (SELECT 1 AS id, 'b' AS test) b ON b.id = a.id LEFT JOIN (SELECT 1 AS id, 'c' AS test) c ON c.id = a.id;第二个查询中,COLUMNS('test')会匹配到来自 a、b、c 三个表的test列。由于多表同名列的存在,JOIN 改写器必须决定每个匹配到的列归属哪张表、是否需要重命名,这一解析过程对"当前正在处理的表"这一上下文非常敏感。若匹配器展开发生在 JOIN 左侧/右侧子查询尚未建立表引用(或表引用缺失)的阶段,就可能在访问表的列信息指针时发生空指针解引用。
需要说明的是,changelog 仅记录了"JOIN 与 COLUMNS matcher 中发生空指针解引用"这一事实,具体崩溃栈细节并未在该文档中展开;上述对触发条件的描述是基于 JoinToSubqueryTransformVisitor.cpp 中匹配器展开逻辑的推断。
四、源码级剖析:JOIN 改写器如何展开 COLUMNS 匹配器
JOIN 查询中 COLUMNS 匹配器的展开工作由 JoinToSubqueryTransformVisitor.cpp 中的extractColumnsRegexpMatcher与extractQualifiedColumnsRegexpMatcher承担。
对于正则型匹配器(JoinToSubqueryTransformVisitor.cpp#L161-L182),其核心逻辑为:
const ASTColumnsRegexpMatcher & columns_regexp_matcher, ... String pattern = columns_regexp_matcher.getPattern(); // 编译正则 auto regexp = std::make_unique<OptimizedRegularExpression>(pattern); if (!regexp) throw Exception("COLUMNS pattern {} cannot be compiled: {}", pattern, regexp.error()); // 遍历表的所有列,逐列匹配正则 // ... // 对匹配到的列应用 transformer for (const auto & transformer : columns_regexp_matcher.transformers->children) applyColumnsTransformer(transformer, columns);而限定表名版本(JoinToSubqueryTransformVisitor.cpp#L185-L214)则要求qualifier必须存在:
if (!qualified_columns_regexp_matcher.qualifier) throw Exception(ErrorCodes::LOGICAL_ERROR, "Qualified COLUMNS matcher must have a qualifier"); auto & identifier = qualified_columns_regexp_matcher.qualifier->as<ASTIdentifier &>();这段代码揭示了两个潜在的空指针风险点,与本缺陷高度相关:
transformers子节点的类型检查:代码中虽遍历transformers->children,但注释明确指出"Qualified COLUMNS matcher must only have children of IASTColumnsTransformer type",若 AST 构造路径存在偏差,applyColumnsTransformer对非 transformer 节点的强制转换可能落空;- 表列信息的解析:匹配正则时依赖预先从表元数据构建的
table_columns映射(JoinToSubqueryTransformVisitor.cpp#L67-L81),并按照"当前表名 → 别名 → 原始表名"的顺序查找列集合。从源码结构可以推断,当 JOIN 中出现无法解析的表引用或列引用为空时,该查找链上的指针访问就可能产生空指针解引用——这正是本次修复所要消除的崩溃路径。
五、修复带来的防御性改进
虽然 changelog 没有给出具体补丁 diff,但从上述源码现状可以归纳该修复落地后形成的防御性保障:
- 严格的类型前置校验:限定型匹配器强制要求
qualifier存在,缺失时抛出LOGICAL_ERROR而非静默解引用; - 正则编译失败显式报错:
COLUMNS pattern {} cannot be compiled异常确保了非法正则不会进入匹配循环; - JSON 反序列化字段校验:在 ASTColumnsMatcher.cpp 中,
pattern、qualifier、column_list缺失时分别抛出BAD_ARGUMENTS,且column_list的子节点必须全部为ASTIdentifier,堵住了畸形 AST 流入执行层的入口; - 回归测试固化:
01839_join_to_subqueries_rewriter_columns_matcher.sql将"JOIN + COLUMNS"组合作为 stateless 测试永久纳入测试套件,防止缺陷复发。
对于开发者而言,该修复的启示在于:凡是"匹配器需要延迟解析为具体对象"的语法糖(COLUMNS、*、EXCEPT/REPLACE),在 JOIN 多表上下文中的展开都必须建立完整的表-列引用解析链,任何一环缺失都应快速失败(fail fast)并给出可读错误,而非放任空指针崩溃。
六、如何在本地验证与回归
当前仓库可直接运行相关测试验证该功能路径的健康状态:
- 验证 COLUMNS 匹配器基础语义:执行 01470_columns_transformers.sql,它覆盖了正则匹配、显式列列表、
APPLY/EXCEPT/REPLACE组合以及EXPLAIN SYNTAX输出; - 验证 JOIN + COLUMNS 组合:执行 01839_join_to_subqueries_rewriter_columns_matcher.sql,该文件直接针对本缺陷场景编写,若测试通过则说明匹配器在 JOIN 改写路径上不再触发崩溃;
- 观察 AST 展开结果:使用
EXPLAIN SYNTAX SELECT COLUMNS('test') FROM ...查看匹配器展开后的实际列表达式,验证列归属与重命名是否符合预期。
运行 stateless 测试的标准方式为:
# 在已构建 clickhouse 二进制的基础上 ./tests/clickhouse-test 01470_columns_transformers 01839_join_to_subqueries_rewriter_columns_matcher七、总结
v22.4.3.3-stable 作为 v22.4 系列的快速补丁版,其核心价值在于消除了 JOIN 查询与 COLUMNS 列匹配器组合使用时的空指针解引用崩溃。通过 changelog 记录、AST 解析实现、JOIN 改写器展开逻辑以及两条 stateless 回归测试的相互印证,可以完整还原该缺陷的成因与修复边界。对于在生产环境使用 v22.4 系列并大量依赖 COLUMNS 匹配器做动态列投影的团队,升级至 v22.4.3.3-stable(或之后的稳定版)是规避该崩溃的推荐做法。
【免费下载链接】ClickHouseClickHouse® is a real-time analytics database management system项目地址: https://gitcode.com/GitHub_Trending/cli/ClickHouse
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考