
ClickHouse v25.8.21.7-lts 发布解读六大 Bug 修复的根因与源码级剖析【免费下载链接】ClickHouseClickHouse® is a real-time analytics database management system项目地址: https://gitcode.com/GitHub_Trending/cli/ClickHouse导读v25.8.21.7-lts 是 ClickHouse 25.8 LTS 分支上的一个补丁版本相比 v25.8.20.4-lts 共合入 6 项 Bug 修复全部属于官方稳定版本中的用户可见错误行为user-visible misbehavior。本文以官方 changelogdocs/changelogs/v25.8.21.7-lts.md为骨架逐条还原每个修复的触发场景、根因与修复思路并结合当前仓库源码日期周号计算、BSI 位图聚合、目录解析、JSON 嵌套列序列化等模块给出可验证的底层依据帮助 LTS 用户在升级前后评估影响面并设计回归验证用例。一、版本定位LTS 分支上的补丁版本ClickHouse 的版本命名中25.8表示 2025 年 8 月发布的主版本ltsLong Term Support后缀代表长期支持分支会持续接收经过严格 backport反向移植的 Bug 修复而不引入新功能。v25.8.21.7-lts的对比基线是v25.8.20.4-lts两者之间合入的 6 项修复全部标注为Bug Fix且每个修复都以Backported in #xxxxx的形式注明其对应的上游问题追踪编号体现了 LTS 分支低风险、高稳定性的合入策略。从修复涉及的模块看本次补丁覆盖了四条典型链路查询优化分区裁剪对toWeek()的错误推断直接影响查询结果正确性聚合计算BSI 位图索引聚合状态在乘以偶数整数常量时的自异或self-XOR崩溃表操作与存储CHECK TABLE与稀疏序列化、嵌套Array(JSON)列插入时的异常目录解析旧分析器路径下TABLE_UUID_MISMATCH被忽略的问题。下面逐条展开。二、分区裁剪修复WHERE toWeek(date, mode) N在第 49–52 周返回空结果2.1 触发场景在按toYYYYMM(date)分区的表上执行SELECT * FROM events WHERE toWeek(date, mode) 50; -- 第 50 周当N落在 49–52 时查询会返回空结果即使表中明明存在该周的数据。这是分区裁剪partition pruning优化器对toWeek()表达式的语义推断有误导致的优化器错误地认为某些分区不可能命中这些周号从而将对应分区提前裁剪掉。2.2 根因toWeek()的年界回绕破坏了单调性假设toWeek()返回一年中的第几周其核心实现位于 src/Functions/DateTimeTransforms.h 的ToWeekImpl真正计算委托给 DateLUT 的toYearWeekstatic UInt8 execute(Int64 t, UInt8 week_mode, const DateLUTImpl time_zone) { YearWeek yw time_zone.toYearWeek(time_zone.toDayNum(t), week_mode); return yw.second; }源码中对该函数单调性的注释值得特别注意src/Functions/DateTimeTransforms.h/// toWeek() is not monotonic because week numbers can wrap at year boundaries /// (e.g. ISO week 52 - week 1 in late December), depending on the week_mode. static constexpr bool hasMonotonicity() { return false; }周号在年末会回绕ISO 第 52 周之后直接跳到下一年的第 1 周因此toWeek()整体上不具备单调性。分区裁剪在推断分区键表达式范围时若没有正确处理这一回绕就会在边界周49–52上产生错误的裁剪决策。2.3 底层周号计算week_mode 的四种标志toYearWeek定义于 src/Common/DateLUTImpl.h它按位解析week_mode支持四种标志组合与 MySQL 的WEEK()模式语义对齐标志位含义MONDAY_FIRST周一作为一周的第一天YEAR周号采用周年week year语义FIRST_WEEKDAY包含当年第一个工作日的那一周为第 1 周NEWYEAR_DAY包含 1 月 1 日的那一周为第 1 周周号范围 1–53且忽略YEAR与FIRST_WEEKDAY核心分支逻辑摘录如下const bool newyear_day_mode week_mode static_castUInt8(WeekModeFlag::NEWYEAR_DAY); const bool monday_first_mode week_mode static_castUInt8(WeekModeFlag::MONDAY_FIRST); bool week_year_mode week_mode static_castUInt8(WeekModeFlag::YEAR); const bool first_weekday_mode week_mode static_castUInt8(WeekModeFlag::FIRST_WEEKDAY);代码中还处理了年初/年末的边界当年 1 月初的几天可能属于上一年的周年第 52/53 周而 12 月底的几天可能已经属于下一年的第 1 周——这正是周号回绕、导致裁剪推断出错的根源区域。此外toYearWeek对超出 DateLUT 范围LUT 之外的日期做了每 400 年周期的移位计算并将结果年份钳制在可表示范围内避免UInt16的YYYYWW结果回绕可配合 src/Common/tests/gtest_DateLUTImpl.cpp 中的单元测试加深理解。2.4 修复含义与回归建议本次修复PR #99542确保分区裁剪对toWeek()的推断不会漏掉 49–52 周。升级后建议用如下查询做回归-- 构造覆盖年末跨周数据的分区表 CREATE TABLE weekly_events (date Date, val UInt32) ENGINE MergeTree PARTITION BY toYYYYMM(date) ORDER BY date; -- 验证 49-52 周均能正确命中数据 SELECT toWeek(date, 3) AS w, count() FROM weekly_events GROUP BY w ORDER BY w;三、BSI 聚合崩溃修复NumericIndexedVector自异或引发的断言与错误结果3.1 触发场景对NumericIndexedVector这类聚合状态执行乘以偶数整数常量的运算时Debug 构建触发断言失败assert(x1 ! x2)直接抛异常Release 构建得到错误结果因为A XOR A 0数据被清零。3.2 根因full adder 对别名位图的 in-place XOR该聚合的数据结构实现位于 src/AggregateFunctions/AggregateFunctionGroupNumericIndexedVectorDataBSI.h。pointwiseAddInplace用逐位全加器full adder参考 Wikipedia 的电子加法器实现直接在 BSI 位图数组上做逐点加法void pointwiseAddInplace(const BSINumericIndexedVector rhs) { /// Self-addition requires a deep copy because the full adder logic below /// performs in-place XOR on shared bitmaps (sum-rb_xor(*addend) where /// sum and addend alias the same Roaring bitmap via shallowCopyFrom), /// which triggers an assertion in CRoaring (assert(x1 ! x2)) and would /// produce incorrect results (A XOR A 0) in release builds. if (this rhs) { BSINumericIndexedVector copy; copy.deepCopyFrom(rhs); pointwiseAddInplace(copy); return; } ... }全加器逻辑中对sum与addend分别执行rb_xor、rb_and如果两个操作数通过shallowCopyFrom别名到同一个 Roaring 位图self-addition 场景in-place 的A XOR A会把位图清零同时触发 CRoaring 的assert(x1 ! x2)。3.3 修复思路自加法先深拷贝修复方案非常直接在this rhs时先deepCopyFrom(rhs)得到一份独立拷贝再对拷贝递归执行pointwiseAddInplace从而彻底避免别名位图上的 in-place XOR。同样的防护也应用到了pointwiseSubtractInplace自减法因为二者共享相同的前提假设void pointwiseSubtractInplace(const BSINumericIndexedVector rhs) { /// Self-subtraction requires a deep copy for the same reason as /// pointwiseAddInplace: in-place XOR on aliased bitmaps is undefined. if (this rhs) { BSINumericIndexedVector copy; copy.deepCopyFrom(rhs); pointwiseSubtractInplace(copy); return; } ... }乘法由按位展开为连续加法实现因此乘以偶数时必然会出现某一轮this与rhs相同的自加法路径这正是偶数常量才能稳定触发该 Bug 的原因。merge同样复用了pointwiseAddInplace因此并行聚合的 merge 阶段也因该修复一并受益。四、目录解析修复旧分析器忽略TABLE_UUID_MISMATCH4.1 触发场景TABLE_UUID_MISMATCH错误表示当前解析出的表与磁盘/目录中记录的 UUID 不匹配。在非 Analyzer 路径即旧分析器下该错误会被静默忽略导致解析结果与实际存储不一致进而引发后续不可预期的行为。4.2 源码佐证错误码定义于 src/Common/ErrorCodes.cpp而解析逻辑位于 src/Interpreters/DatabaseCatalog.cpp/// In old analyzer resolving done in multiple places, so we ignore TABLE_UUID_MISMATCH error. exception-emplace(Exception(ErrorCodes::TABLE_UUID_MISMATCH, Table {} does not match {}, table_id.getNameForLogs(), table_storage_id.getNameForLogs()));注释指出旧分析器的解析分散在多个位置这正是忽略行为产生的背景本次修复PR #99380调整了该错误的处理路径使其在非 Analyzer 模式下也能被正确识别与上报不再被吞掉。五、CHECK TABLE修复Tuple 内 Dynamic 列的稀疏序列化校验5.1 触发场景对包含Tuple 类型、且其中嵌套 Dynamic 列的表执行CHECK TABLE时如果 Dynamic 列启用了稀疏序列化sparse serialization校验会失败或产生误报。相关问题追踪编号为 #96588。5.2 技术背景稀疏序列化是 ClickHouse 为高基数/多空值列设计的存储优化——只物化非默认值并额外记录位置信息。当该列位于 Tuple 内部、且类型为可动态演化的Dynamic时序列化/反序列化路径会与普通列不同CHECK TABLE需要按稀疏方式读取并比对而原实现未能正确识别这一场景导致表结构自检异常。本次修复PR #99351补齐了 Tuple-with-Dynamic 场景下稀疏序列化的校验逻辑。5.3 回归建议-- 构造含 Tuple(Dynamic) 的宽表并执行自检 CREATE TABLE t_dyn (id UInt64, t Tuple(d Dynamic)) ENGINE MergeTree ORDER BY id SETTINGS min_bytes_for_wide_part 0; -- 强制 wide part 以覆盖该路径 CHECK TABLE t_dyn;六、嵌套Array(JSON)插入修复wide part 下LOGICAL_ERROR: Stream ... not found6.1 触发场景当满足以下三个条件同时成立时INSERT 会抛出LOGICAL_ERROR异常Stream ... not found表使用wide part存储按行数/字节阈值自动切换插入的目标列是嵌套的Array(JSON)即数组元素为 JSON 对象关闭了optimize_on_insertSET optimize_on_insert 0。6.2 根因JSON 类型在落地时会被拆分为多个子流如类型元数据流、各键对应的子列流。在optimize_on_insert 0时插入路径跳过部分优化步骤导致读取端按列名查找子流时找不到对应的流Stream not found从而抛出LOGICAL_ERROR。本次修复PR #100475对齐了该场景下子流的创建与查找逻辑使嵌套Array(JSON)在 wide part 关闭插入优化时也能正常写入。七、关于列表中未提供描述的一项修复在 v25.8.21.7-lts 的 changelog 中还有一项修复PR #100024由 Shaohua Wang 提交backport 追踪编号 #100164未在 changelog 中提供任何修复描述仅有 PR 编号与作者信息。由于仓库无法提供该项的具体变更说明本文不对其内容做任何推断仅如实指出该条目存在读者可关注后续 changelog 的补充说明。八、升级建议与验证清单v25.8.21.7-lts 作为 LTS 分支补丁全部变更均为 Bug 修复、不含新功能适合在测试环境先行验证后推广到生产。建议重点回归以下场景修复项重点回归场景关联源码仓库内可查阅toWeek()分区裁剪按toYYYYMM分区 WHERE toWeek(date, mode) IN (49..52)src/Functions/DateTimeTransforms.h、src/Common/DateLUTImpl.hBSI 聚合自异或对NumericIndexedVector聚合状态乘以偶数常量并行聚合 mergesrc/AggregateFunctions/AggregateFunctionGroupNumericIndexedVectorDataBSI.hTABLE_UUID_MISMATCH非 Analyzerenable_analyzer0下的表解析src/Interpreters/DatabaseCatalog.cppCHECK TABLE DynamicTuple(Dynamic) 稀疏序列化自检src/Columns嵌套Array(JSON)插入wide part optimize_on_insert0src/DataTypes、src/Formats说明以上版本号、修复内容与 backport 关系均以当前仓库中的 docs/changelogs/v25.8.21.7-lts.md 为准源码层面的根因分析对应 src/Functions/DateTimeTransforms.h、src/Common/DateLUTImpl.h、src/AggregateFunctions/AggregateFunctionGroupNumericIndexedVectorDataBSI.h 与 src/Interpreters/DatabaseCatalog.cpp 中的实际实现。【免费下载链接】ClickHouseClickHouse® is a real-time analytics database management system项目地址: https://gitcode.com/GitHub_Trending/cli/ClickHouse创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考