ClickHouse v25.8.21.7-lts 发布解读:六大 Bug 修复的根因与源码级剖析
2026/9/19 0:57:11 网站建设 项目流程

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)。本文以官方 changelog(docs/changelogs/v25.8.21.7-lts.md)为骨架,逐条还原每个修复的触发场景、根因与修复思路,并结合当前仓库源码(日期周号计算、BSI 位图聚合、目录解析、JSON 嵌套列序列化等模块)给出可验证的底层依据,帮助 LTS 用户在升级前后评估影响面并设计回归验证用例。

一、版本定位:LTS 分支上的补丁版本

ClickHouse 的版本命名中,25.8表示 2025 年 8 月发布的主版本,lts(Long Term Support)后缀代表长期支持分支,会持续接收经过严格 backport(反向移植)的 Bug 修复而不引入新功能v25.8.21.7-lts的对比基线是v25.8.20.4-lts,两者之间合入的 6 项修复全部标注为Bug Fix,且每个修复都以Backported in #xxxxx的形式注明其对应的上游问题追踪编号,体现了 LTS 分支"低风险、高稳定性"的合入策略。

从修复涉及的模块看,本次补丁覆盖了四条典型链路:

  1. 查询优化:分区裁剪对toWeek()的错误推断(直接影响查询结果正确性);
  2. 聚合计算:BSI 位图索引聚合状态在乘以偶数整数常量时的自异或(self-XOR)崩溃;
  3. 表操作与存储CHECK TABLE与稀疏序列化、嵌套Array(JSON)列插入时的异常;
  4. 目录解析:旧分析器路径下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 的toYearWeek

static 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,且忽略YEARFIRST_WEEKDAY

核心分支逻辑摘录如下:

const bool newyear_day_mode = week_mode & static_cast<UInt8>(WeekModeFlag::NEWYEAR_DAY); const bool monday_first_mode = week_mode & static_cast<UInt8>(WeekModeFlag::MONDAY_FIRST); bool week_year_mode = week_mode & static_cast<UInt8>(WeekModeFlag::YEAR); const bool first_weekday_mode = week_mode & static_cast<UInt8>(WeekModeFlag::FIRST_WEEKDAY);

代码中还处理了年初/年末的边界:当年 1 月初的几天可能属于上一年的"周年"(第 52/53 周),而 12 月底的几天可能已经属于下一年的第 1 周——这正是周号回绕、导致裁剪推断出错的根源区域。

此外,toYearWeek对超出 DateLUT 范围(LUT 之外)的日期做了每 400 年周期的移位计算,并将结果年份钳制在可表示范围内,避免UInt16YYYYWW结果回绕,可配合 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; } ... }

全加器逻辑中对sumaddend分别执行rb_xorrb_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; } ... }

乘法由"按位展开为连续加法"实现,因此乘以偶数时必然会出现某一轮thisrhs相同的自加法路径,这正是偶数常量才能稳定触发该 Bug 的原因。merge同样复用了pointwiseAddInplace,因此并行聚合的 merge 阶段也因该修复一并受益。

四、目录解析修复:旧分析器忽略TABLE_UUID_MISMATCH

4.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 found

6.1 触发场景

当满足以下三个条件同时成立时,INSERT 会抛出LOGICAL_ERROR异常"Stream ... not found"

  1. 表使用wide part存储(按行数/字节阈值自动切换);
  2. 插入的目标列是嵌套的Array(JSON)(即数组元素为 JSON 对象);
  3. 关闭了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.h
BSI 聚合自异或NumericIndexedVector聚合状态乘以偶数常量;并行聚合 mergesrc/AggregateFunctions/AggregateFunctionGroupNumericIndexedVectorDataBSI.h
TABLE_UUID_MISMATCH非 Analyzer(enable_analyzer=0)下的表解析src/Interpreters/DatabaseCatalog.cpp
CHECK TABLE+ DynamicTuple(Dynamic) 稀疏序列化自检src/Columns
嵌套Array(JSON)插入wide part +optimize_on_insert=0src/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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询