【openGauss】排查一起panweidb2.0子链接提升的coredump案例
背景
最近同事收到panweidb2.0一起故障请求,说是有在B库里,一个7张表关联的查询SQL,大概六十几行,只要只执行,数据库就coredump了,同事就把建表语句和SQL发我,让我来分析一下。
另外,同事提醒,这个用例在panweidb 3.0没有coredump。
初步判断
panweidb2.0 会coredump ,但是3.0没有,自然会想到,可能是openGauss 5.0有BUG,在openGauss 6.0修掉了。所以我在我本地的openGauss 5.0.1里测了下,的确可以复现,在7.0里则正常执行。 (后面我在GaussDB 503.1.0SPC1700 版本测了,也会coredump,505以上就没问题了)
接下来的目标则是:
- 找到coerdump的源码级原因
- 构造出最小化测试用例
- 找到是哪个commit进行的修复
以往,就是复现问题后,用gdb去看coredump文件,在去对源码,分析源码上下文逻辑,识别到问题,再看git历史记录。说着简单,但真正纯手动去做,耗时估计也得有个半天到一天吧。
不过,现在有AI了,只要能复现,上面这些事就可以完全交给AI去干。
本次我用colpilot-cli连我本地的deepseek-flash来分析。
根因诊断(AI)
结论
这不是数据/磁盘问题,是 openGauss 5.0.1 规划器(子链接提升)缺陷导致的空指针解引用。
| 项 | 值 |
|---|---|
| 信号 | SIGSEGV, si_code=1 (SEGV_MAPERR), si_addr=0x4 |
| 崩溃点 | list_nth_cell ← list_nth(NULL, 0),var.cpp:1295 |
| 上层函数 | flatten_join_alias_vars_mutator(展开 JOIN RTE 别名) |
| 进程 | pid 789944(主进程)/ 崩溃线程 tid 790080 |
| 时间 | 2026-10-20 16:29:50(实例 16:28:48 启动,62 秒后崩溃) |
环境
- openGauss 5.0.1 build 33b035fd,Kylin V10 x86_64,单机单 DN(
/opt/og501/data,dn_6001) - 库为 B 兼容:
sql_compatibility=B,走 dolphin 插件的 planner(已安装 dolphin.so,栈中可见) - 关键 GUC:
rewrite_rule=magicset、enable_bbox_dump=on、dolphin.optimizer_switch=default - 符号表 已于崩溃后 16:32 手工装入
$GAUSSHOME/bin(gaussdb0.symbol,debuglink 一致,版本串与二进制逐字相同);该 build 无 Build-ID
证据链
#3 list_nth_cell (n=0, list=0x0) list.cpp:637
#4 list_nth (list=0x0, n=0) list.cpp:639
#5 flatten_join_alias_vars_mutator (node=0x153b7ca6e3b0, context=...) var.cpp:1295
#6..19 expression_tree_mutator ×7(T_BoolExpr/T_OpExpr/T_CoalesceExpr)
#20 flatten_join_alias_vars var.cpp:1229
#22 preprocess_expression dolphin.so planner.cpp:2006
#25 subquery_planner → #28 planner → #29 pg_plan_query → #31 exec_simple_query
崩溃指令(flatten_join_alias_vars_mutator+1436):
mov 0x68(%r13),%rdi ; r13 = RangeTblEntry 0x153b7ca6eb60, 0x68 = joinaliasvars
lea -0x1(%rax),%esi ; varattno-1 = 0
call list_nth ; list_nth(NULL,0) → 读 list-\>length @ 0x4 → si_addr=0x4
触发 SQL(core 中 exec_simple_query 的 query_string 原文,节选):
select count(1) from (SELECT pi.id ... FROM piii
INNER JOIN pi ... LEFT JOIN bi ... LEFT JOIN ci ...
WHERE ... AND NOT EXISTS (SELECT 1 FROM cyi ...)
AND ( (current_date - piii.padt) \<= 30
OR ( ... AND EXISTS (SELECT 1 FROM mi ...)
AND ( COALESCE( (SELECT MAX(to_date(substring(mi2.pet,1,10),'yyyy-MM-dd'))
FROM mi2 WHERE mi2.pii = pi.id ...),
(SELECT MAX(...) FROM mi3 WHERE ...) ) \>= current_date ))) t;
崩溃时 parse tree 的实际状态(core 中遍历 rtable,共 14 项):
9 cyi kind=RTE_RELATION
10 subquery kind=RTE_SUBQUERY (mi)
11 subquery kind=RTE_SUBQUERY (mi2, tlist: max, ?column?)
12 unnamed_join kind=RTE_JOIN jointype=JOIN_LEFT colnames=0 joinaliasvars=NIL ← 0 列 JOIN RTE
13 subquery kind=RTE_SUBQUERY (mi3)
14 unnamed_join kind=RTE_JOIN jointype=JOIN_LEFT colnames=0 joinaliasvars=NIL ← 0 列 JOIN RTE
COALESCE 的两个参数: Var(varno=12,varattno=1,type=date), Var(varno=14,varattno=1,type=date)
根因
rewrite_rule=magicset打开后,计划阶段执行子链接提升:subselect.cppconvert_expr_subink_with_agg_targetlist()把 COALESCE 里的两个MAX(...)标量子查询提升成LEFT JOIN,并在subselect.cpp:5942用
addRangeTableEntryForJoin(NULL, NIL, JOIN_LEFT, NIL, NULL, true)追加一个占位用的 0 列 JOIN RTE(parse_relation.cpp:1679直接令joinaliasvars = aliasvars = NIL)。5776/6341 行是同样写法。- 重写后的 qual 中出现
Var(varno=12/14, varattno=1),指向的正是这两个没有任何列的 JOIN RTE。 - 随后
subquery_planner → preprocess_qual_conditions → preprocess_expression对顶层 qual 无条件调用flatten_join_alias_vars();var.cpp:1295假定joinaliasvars至少有varattno个元素,直接list_nth(NIL, 0)→ 段错误。
已证实:崩溃点、非法 Var、0 列 JOIN RTE 及其产生代码。未证实:为何 varno 落在 12/14(空 JOIN RTE)而非 11/13(子查询 RTE)——这是该重写内部 varno 计算/后续偏移的具体缺陷,需带调试信息单步才能定性。
影响面与旁证
- 只有 1 个 core,即本次事件;
pg_log在 16:29:35 后完全静默,之后无重启记录,实例目前处于停止状态。 - 启动日志有
database system was not properly shut down; automatic recovery in progress(16:28:49),说明更早一次是非正常关闭(最后一次日志为 2026-01-10),但无对应 core,无法确认是否同类崩溃。
处置建议
- 规避(会话级,先在测试环境验证):该版本
rewrite_rule支持disable_pullup_expr_sublink(二进制校验串已确认),可试
SET rewrite_rule = 'disable_pullup_expr_sublink';后再跑该 SQL。 - SQL 改写(更稳妥的临时方案):把 COALESCE 内两个
MAX(...)标量子查询改成一次预聚合的 LEFT JOIN / 临时表两步取数,避开提升路径。 - 上报内核:附 core、
ffic_gaussdb-2026-10-20_162950.log、完整 SQL、gaussdb --version(5.0.1 build 33b035fd)与上述栈;这是 planner 产生非法 parse tree,非用户数据问题,加索引/调参无效。
操作说明:全程只读,未改动实例、配置或 core;仅在 /tmp 生成过分析文件,已清理,保留一份完整栈 /tmp/og501-core-bt.txt。如需我启动 og501 实例做复现与 rewrite_rule 规避验证(会再次触发崩溃并生成新 core),请确认。
最小化用例(AI)
-- DDL:0 行数据即可,崩溃发生在计划阶段
create table t1 (id int, d date);
-- 崩溃语句
select * from (
select 1 from t1 a
where (false
or coalesce((select max(b.d) from t1 b where b.id = a.id),
(select max(b.d) from t1 b where b.id = a.id)) >= current_date)
) t;
实测结果:connection to server was lost,DN 进程整体退出,生成 core-gaussdb-\<pid\>-*-bbox.lz4。共复现 12 次,13 份 ffic 日志(含原始那次)栈完全一致:si_addr=0x4、list_nth ← flatten_join_alias_vars_mutator。
收敛过程(每一条都是实测)
| 变体 | 结果 |
|---|---|
| 单个聚合标量子查询 + OR + 派生表 | 不崩 |
COALESCE(子查询, 常量) | 不崩 |
COALESCE(两个聚合子查询),AND 连接 | 不崩 |
COALESCE(两个聚合子查询) + OR,无派生表 | 不崩 |
| 两个非聚合标量子查询 | 不崩 |
| 两个无关联聚合子查询 | 不崩 |
| 三表版本(原始形状 3 表) | 崩 |
| 单表自关联(上例) | 崩 |
count(*) 代替 max()(比较改 \>= 0) | 崩 |
A 兼容库(postgres)里同样语句 | 也崩,栈相同 |
set rewrite_rule='disable_pullup_expr_sublink' | 不崩,正常返回 |
崩溃三要素:① 外层派生表(子查询上拉,varno 偏移);② WHERE 顶层是 OR(走 convert_OREXPR/OREXISTS_to_join);③ COALESCE 内两个带聚合函数的相关标量子查询(各被提升为一个 LEFT JOIN,并追加 0 列的占位 JOIN RTE,joinaliasvars=NIL)。任一去掉都不崩,与数据量、列类型、是否 B 兼容无关。
补充:set rewrite_rule=''(显示 none)仍然崩,说明与 MAGIC_SET 无关,只是 disable_pullup_expr_sublink 这个开关能剪掉该路径。
修复commit(AI)
已修复,修复提交是 dfb53df30
上游提交(opengauss/openGauss-server,Gitee):
dfb53df3074ccab75d78fc6e9b5c32b962845052 — 作者 wangfeihuo,2024-09-26,标题“修复pullup导致的core问题”,并入 master 的 PR !6476(merge 提交 3c8307560)。
6.0.0 回合(cherry-pick):da08958ae1a64b52ea88b7a40a59652fe912aeb8,PR !6477(merge e0c591eea)。
改动只有 7 行删除,落在 src/gausskernel/optimizer/plan/subselect.cpp 的 convert_ORCLAUSE_to_join():
// 删除前(有 bug)
if (notNullExpr != NULL) {
notNullAnd = make_and_qual(notNullAnd, notNullExpr); // ← 删
replace = true;
}
if (notNullAnd != NULL) {
or_clause = (BoolExpr*)replace_node_clause(or_clause, clause, notNullAnd, RNC_RECURSE_AGGREF); // ← 删
}
即:不再把被提升子链接所在的 OR 分支整体替换成 “not null” 表达式。上一版 5.0.1 的那次替换会留下指向“0 列 JOIN RTE”的 Var,随后 flatten_join_alias_vars_mutator → list_nth(NIL, 0) 崩溃。这与我们 core 里观察到的 varno=12/14(两个 0 列 JOIN RTE)、si_addr=0x4 完全对应。
同一函数族里还有一个更早的半修复 3f02e6568(2024-05-07,“子查询中使用orderby数据丢失问题”),它只改了 convert_OREXPR_to_join,去掉了 notNullTest 的返回,不构成完整修复。
该提交自带回归用例,与你的场景同族
src/test/regress/sql/sublink_pullup_enhance.sql 新增:
select count(0) from (
select t1.* from t1
where (((select count(1) from t2 where t2.c1=t1.c1) +
(select count(1) from t3 where t3.c1=t1.c1)) \> 0
or t1.c1 = 5)
) tmp_count;
派生表 + OR + 同一表达式里两个相关标量子查询,就是这次的形状。
我在本机做的版本对照(实测)
| 版本 | 构建日期 | 该 SQL 结果 |
|---|---|---|
| openGauss 5.0.1 (33b035fd) | 2023-12-15 | 崩溃(日志:connection to server was lost) |
| openGauss 5.1.0 (b5a8d5b0) | 2023-09-28 | 崩溃 |
| openGauss-lite 6.0.0 (aee4abd5) | 2024-09-29 | 正常,返回 count=3;我们的最小用例也正常 |
| openGauss 7.0.0-RC3 (3700f950) | 2026-07-27 | 正常 |
即:上游这条回归用例的 SQL,在 5.0.1 上照样把 DN 打挂 —— 直接证明该提交修的就是我们这个 bug。
5.x 系列没有回合,这一点很重要
按 tag 逐个核对 subselect.cpp 里 notNullAnd / notNullTest 是否残留:
v5.0.0 v5.0.2 v5.0.3 v5.0.5 v5.1.0 → 仍有(4 / 3 处),即未修复
v6.0.0 v6.0.5 v6.0.6 v7.0.0 → 已无,即已修复
分支 HEAD origin/5.0.0、origin/5.1.0 也仍是老代码。结论:所有 5.0.x / 5.1.0 发布版都没有这个修复,你手上的 5.0.1 无法通过打小补丁消除,只能升级到 6.0.0 及以上(建议 6.0.5/6.0.6 或 7.0.0),或升级前用会话级规避。
另:2025-08-28 还有一条同族后续修复 009c009b9 “fix count subquery with or sublink pullup”(仅 v7.0.0),去掉 isnull 参数传递,属于该路径的第二处缺陷。6.0.x 若遇到同族变体,需评估这条是否也要回合。
给你的可执行结论
- 根治:升级 ≥ 6.0.0;升级前用
set rewrite_rule='disable_pullup_expr_sublink';(已实测可绕过,见/home/og501/repro_coredump_og501/),或改写 SQL 避开 OR 中的双标量子查询。 - 报障时可直接引用
dfb53df30/ PR !6476 / !6477,并说明 5.x 未回合,要求给出 5.0.1 的修复版本或回合补丁。 - 需要的证据材料(core、ffic 日志、最小用例、版本对照表)都已在机上的
repro_coredump_og501目录。
总结
AI回复的这些内容,我核实了,的确都是对的。对于这种可复现的开源数据库内核BUG,本地部署的flash级别模型都可以轻松诊断定位。(本文有点水,一次普普通通的常规AI使用罢了)

