本文首发时间 2026-07-18
背景
隔壁驻场同事在帮客户做 SQL 优化时,发现某测试环境的 GaussDB 数据库(506.0.0SPC0500)近期发生过 coredump。查看 ffic_log,里面是一条很简单的 update 语句,于是我就来稍微分析一下。
由于本地没有完整的 506.0.0SPC0500 环境,我先用 506.0.0SPC0100 进行分析——这两个版本都能复现这个 coredump。
复现及日志
环境:
GaussDB 506.0.0SPC0100
数据准备:
drop table if exists test_reorder;
create table test_reorder(id number, c1 varchar2(200), c2 varchar2(200));
insert into test_reorder select random(), 'aaa', 'bbb' from pg_catalog.generate_series(1, 100) e;
复现 SQL:
update test_reorder a set id =
(select s.newid from (select b.id, row_number()over (order by b.id) newid from test_reorder b) s
where a.id = s.id);
ffic_log:
====== Statement info ======
[statement] unique SQL key - sql id: 0, cn id: 0, user id: 0
[statement] debug query id: 0
[postgres_cxt] debug_query_string: update test_reorder a set id =
(select s.newid from (select b.id, row_number()over (order by b.id) newid from test_reorder b) s
where a.id = s.id)
[ffic_cxt] parse_tree:
({UPDATE :relation <> :targetList ({RESTARGET :name id :indirection <> :val {SUBLINK :subLinkType 4 :testexpr <> :operName <> :subselect {QUERY :commandType 1 :querySource 0 :canSetTag true :utilityStmt <> :resultRelation 0 :hasAggs false :hasWindowFuncs false :hasTargetSRFs false :is_flt_frame true :hasSubLinks false :hasDistinctOn false :hasRecursive false :hasModifyingCTE false :hasForUpdate false :hasRowSecurity false :hasSynonyms false :cteList <> :rtable ({RTE :alias {ALIAS :aliasname s :colnames <> :block_name <> :schema_name <> :auto_aliasname <>} :eref {ALIAS :aliasname s :colnames ("id" "newid") :block_name <> :schema_name <> :auto_aliasname <>} :rtekind 1 :relname <> :partAttrNum <> :subquery {QUERY :commandType 1 :querySource 0 :canSetTag true :utilityStmt <> :resultRelation 0 :hasAggs false :hasWindowFuncs true :hasTargetSRFs false :is_flt_frame true :hasSubLinks false :hasDistinctOn false :hasRecursive false :hasModifyingCTE false :hasForUpdate false :hasRowSecurity false :hasSynonyms false :cteList <> :rtable ({RTE :alias {ALIAS :aliasname b :colnames <> :block_name <> :schema_name <> :auto_aliasname <>} :eref {ALIAS :aliasname b :colnames ("id" "c1" "c2") :block_name <> :schema_name <> :auto_aliasname <>} :rtekind 0 :relname test_reorder :partAttrNum <> :relid 180224 :relkind r :isResultRel false :tablesample <> :timecapsule <> :partitionOid 0 :isContainPartition false :subpartitionOid 0 :isContainSubPartition false :refSynOid 0 :ispartrel false :ignoreResetRelid false :pname <> :ref_nestsynoid <> :partid_list <> :plist <> :lateral false :inh true :inFromCl true :requiredPerms 2 :checkAsUser 0 :selectedCols (b...) :modifiedCols (b...) :insertedCols (b...) :updatedCols (b...) :orientation 2 :securityQuals <> :subquery_pull_up false :correlated_with_recursive_cte false :slicegroup 0 :relhasbucket false :isbucket false :buckets <> :isexcluded false :sublink_pull_up false :is_ustore true :extraUpdatedCols (b...) :partitionOidList <> :subpartitionOidList <> :partitionNameList <> :isexpandrte false :dblink_name <> :dblink_schema <> :dblink_relname <> :is_dml_subquery_rte false}) :jointree {FROMEXPR :fromlist ({RANGETBLREF :rtindex 1}) :quals <>} :targetList ({TARGETENTRY :expr {VAR :varno 1 :varattno 1 :vartype 1700 :vartypmod -1 :varcollid 0 :varlevelsup 0 :varnoold 1 :varoattno 1 :location 62} :resno 1 :resname id :ressortgroupref 1 :resorigtbl 180224 :resorigcol 1 :resjunk false :rtindex 0} {TARGETENTRY :expr {WINDOWFUNC :winfnoid 3100 :wintype 20 :wincollid 0 :inputcollid 0 :args <> :winref 1 :winstar false :winagg false :location 68 :ignore_nulls false :keep_func false :keep_first false :is_distinct false} :resno 2 :resname newid :ressortgroupref 0 :resorigtbl 0 :resorigcol 0 :resjunk false :rtindex 0}) :starStart <> :starEnd <> :starOnly <> :returningList <> :groupClause <> :groupingSets <> :havingQual <> :windowClause ({WINDOWCLAUSE :name <> :refname <> :partitionClause <> :orderClause ({SORTGROUPCLAUSE :tleSortGroupRef 1 :eqop 1752 :sortop 1754 :nulls_first false :hashable true :groupSet false}) :frameOptions 530 :startOffset <> :endOffset <> :winref 1 :copiedOrder false :runCondition <> :winfnoid 0 :startInRangeFunc 0 :endInRangeFunc 0 :inRangeColl 0 :inRangeAsc false :inRangeNullsFirst false}) :distinctClause <> :sortClause <> :limitOffset <> :limitCount <> :rowMarks <> :setOperations <> :constraintDeps <> :hintState <> :sql_statement <> :is_local false :has_to_save_cmd_id false :vec_output false :tdTruncCastStatus 0 :equalVars <> :mergeTarget_relation 0 :mergeSourceTargetList <> :mergeActionList <> :insertAllConditionalActionList <> :is_first false :upsertQuery <> :upsertClause <> :isRowTriggerShippable false :use_star_targets false :is_from_full_join_rewrite false :is_from_rollup_pullup_rewrite false :can_push false :unique_check false :resultRelations <> :blockname <> :auto_blockname <> :p_row_col_conversion_kind 0 :swoptions <> :is_from_start_with_rewrite false :has_rownum false :stopkey <> :rownum_qual <> :right_ref_state <> :is_replace false :has_no_rewrite_rule_hint false :can_ignore false :is_dml_sql false :with_check_option 0 :withCheckOptions <> :preservedTables <> :is_iud_subquery false :has_nestagg false :nestagg_pullup_forbidden false :has_sql_calc_found_rows false :is_from_or2union false :onConflict <>} :security_barrier false :lateral false :inh false :inFromCl true :requiredPerms 0 :checkAsUser 0 :selectedCols (b...) :modifiedCols (b...) :insertedCols (b...) :updatedCols (b...) :orientation 0 :securityQuals <> :subquery_pull_up false :correlated_with_recursive_cte false :slicegroup 0 :relhasbucket false :isbucket false :buckets <> :isexcluded false :sublink_pull_up false :is_ustore false :extraUpdatedCols (b...) :partitionOidList <> :subpartitionOidList <> :partitionNameList <> :isexpandrte false :dblink_name <> :dblink_schema <> :dblink_relname <> :is_dml_subquery_rte false}) :jointree {FROMEXPR :fromlist ({RANGETBLREF :rtindex 1}) :quals {OPEXPR :opno 1752 :opfuncid 1718 :opresulttype 16 :opresulttypmod -1 :opretset false :opcollid 0 :inputcollid 0 :args ({VAR :varno 1 :varattno 1 :vartype 1700 :vartypmod -1 :varcollid 0 :varlevelsup 1 :varnoold 1 :varoattno 1 :location 138} {VAR :varno 1 :varattno 1 :vartype 1700 :vartypmod -1 :varcollid 0 :varlevelsup 0 :varnoold 1 :varoattno 1 :location 143}) :location 142}} :targetList ({TARGETENTRY :expr {VAR :varno 1 :varattno 2 :vartype 20 :vartypmod -1 :varcollid 0 :varlevelsup 0 :varnoold 1 :varoattno 2 :location 41} :resno 1 :resname newid :ressortgroupref 0 :resorigtbl 0 :resorigcol 0 :resjunk false :rtindex 0}) :starStart <> :starEnd <> :starOnly <> :returningList <> :groupClause <> :groupingSets <> :havingQual <> :windowClause <> :distinctClause <> :sortClause <> :limitOffset <> :limitCount <> :rowMarks <> :setOperations <> :constraintDeps <> :hintState <> :sql_statement <> :is_local false :has_to_save_cmd_id false :vec_output false :tdTruncCastStatus 0 :equalVars <> :mergeTarget_relation 0 :mergeSourceTargetList <> :mergeActionList <> :insertAllConditionalActionList <> :is_first false :upsertQuery <> :upsertClause <> :isRowTriggerShippable false :use_star_targets false :is_from_full_join_rewrite false :is_from_rollup_pullup_rewrite false :can_push false :unique_check false :resultRelations <> :blockname <> :auto_blockname <> :p_row_col_conversion_kind 0 :swoptions <> :is_from_start_with_rewrite false :has_rownum false :stopkey <> :rownum_qual <> :right_ref_state <> :is_replace false :has_no_rewrite_rule_hint false :can_ignore false :is_dml_sql false :with_check_option 0 :withCheckOptions <> :preservedTables <> :is_iud_subquery false :has_nestagg false :nestagg_pullup_forbidden false :has_sql_calc_found_rows false :is_from_or2union false :onConflict <>} :location 33 :sublinkId 0} :location 26 :attr_name <> :dblink_name <>}) :whereClause <> :fromClause <> :returningList <> :withClause <> :hintState <> :sortClause <> :limitClause <> :relationClause ({RANGEVAR :schemaname <> :relname test_reorder :partitionname <> :subpartitionname <> :inhOpt 2 :relpersistence p :alias {ALIAS :aliasname a :colnames <> :block_name <> :schema_name <> :auto_aliasname <>} :location 7 :ispartition false :issubpartition false :partitionKeyValuesList <> :isbucket false :buckets <> :withVerExpr false :partitionNameList <> :isexpandrel false :servername <> :mode 0}) :can_ignore false})
======= Call stack =======
[/opt/cluster/usr/local/core/app/bin/gaussdb + 0x12a21b0] _Z32lexical_parameterize_ssu_mutatorP4NodeP22SharedSetUpdateContext + 0x150
[/opt/cluster/usr/local/core/app/bin/gaussdb + 0x1211460] _Z33node_expression_tree_mutator_funcPKciP4NodePFS2_S2_PvES3_b + 0x9a0
[/opt/cluster/usr/local/core/app/bin/gaussdb + 0x1210dfd] _Z33node_expression_tree_mutator_funcPKciP4NodePFS2_S2_PvES3_b + 0x33d
[/opt/cluster/usr/local/core/app/bin/gaussdb + 0x1211a45] _Z33node_expression_tree_mutator_funcPKciP4NodePFS2_S2_PvES3_b + 0xf85
[/opt/cluster/usr/local/core/app/bin/gaussdb + 0x1211460] _Z33node_expression_tree_mutator_funcPKciP4NodePFS2_S2_PvES3_b + 0x9a0
[/opt/cluster/usr/local/core/app/bin/gaussdb + 0x12a5807] _Z26lexical_make_update_sharedP10ParseStateP10UpdateStmtRP4List + 0x47
[/opt/cluster/usr/local/core/app/bin/gaussdb + 0x12b121f] _Z12lexical_stmtP10ParseStateP4Nodebb + 0x15ef
[/opt/cluster/usr/local/core/app/bin/gaussdb + 0x12b4453] _Z22lexical_top_level_stmtP10ParseStateP4Nodebb + 0xd3
[/opt/cluster/usr/local/core/app/bin/gaussdb + 0x12b4ab9] _Z31lexical_parse_analyze_varparamsP4NodePKcPPjPibPP4ListS4_S2_PPbP24MformatParameterizedInfo + 0x99
[/opt/cluster/usr/local/core/app/bin/gaussdb + 0x254c2a2] _ZL15proc_parse_treePP4NodeP4ListPKcS5_PP16CachedPlanSourcePiPPjSB_PP5QueryPbPS3_PP9ExecNodesSF_bP24MmgrMemoryControllerDataP17PreparedStatementSB_.constprop.60 + 0x182
[/opt/cluster/usr/local/core/app/bin/gaussdb + 0x255755a] _Z18gs_process_commandiP12GsStringDataPVb + 0x10ca
[/opt/cluster/usr/local/core/app/bin/gaussdb + 0x255c9c0] _Z12PostgresMainiPPcPKcS2_ + 0x1c30
[/opt/cluster/usr/local/core/app/bin/gaussdb + 0x24e02df] _ZL11backend_runP4Port + 0x25f
[/opt/cluster/usr/local/core/app/bin/gaussdb + 0x251f1b0] _Z27gauss_db_worker_thread_mainIL15knl_thread_role2EEiP14knl_thread_arg + 0x140
[/opt/cluster/usr/local/core/app/bin/gaussdb + 0x24e039a] _ZL20internal_thread_funcPv + 0x1a
[/usr/lib64/libpthread.so.0 + 0x8f1b] 0x7f9836e38f1b
[/usr/lib64/libc.so.6 + 0xf81c0] clone + 0x40
个人分析
这个堆栈和客户环境里的堆栈完全一样,所以 SPC0500 和 SPC0100 的问题原因应该也是相同的。
从堆栈上看,是在对 SQL 进行 PARSE(解析)阶段出的问题,因此应该可以稳定复现。再往内层看,是在做参数化路径处理时出了问题。
尝试用 explain 执行这条 SQL,同样会触发 coredump。此时 ffic_log 里就无法输出完整的 parse_tree,而是显示为:
[ffic_cxt] parse_tree:
({pack_node() could not dump unrecognized node type: 766.
})
766 这个语法节点无法识别。这和我之前遇到的一个报错类似,都是内核在解析 SQL 改写时遇到多重改写就可能报错——只要手动提前改写一处即可规避:
drop table if exists t1;
drop table if exists t2;
create table t1(id number, name varchar2(20));
create table t2(id number, name varchar2(20));
insert into t1 values(1, 'x1'), (2, 'x2'), (3, 'x3');
insert into t2 values(1, 'x1'), (2, 'x2');
-- "(+)" -> "left join"
-- "decode" -> "case when"
-- "between" -> ">= <="
select * from t1, t2
where t1.id = t2.id(+)
and decode(t1.name, 'x1', 1, 10) between 1 and 3;
-- ERROR: Unrecognized node type: 303.
所以对于这次的问题,理论上也可以改写一下,让它不走对应的解析路径。
尝试使用 CTE 进行等价改写:
with t as (select b.id, row_number()over (order by b.id) newid from test_reorder b)
update test_reorder a set id = (select t.newid from t where t.id = a.id);
将包含窗口函数的子查询提取出来放到 WITH 里,这样执行就不会 coredump 了。
AI 第一次分析
以上都是我人工进行的分析,再来看看 AI 会怎么分析吧。
我让 Copilot CLI 接上 mimo-v2.5-pro,去 gdb 分析 coredump 文件。
我:ssh 到 192.168.163.118,su - Ruby,source gauss_env_file,分析下 /opt/cluster/var/lib/engine/data1/data/dn_6002/core-gaussdb-8512-2026_07_10_11_10_40-bbox,gdb 和符号表已经装好。
Copilot 分析报告:
GaussDB Core Dump 分析报告
基本信息
- Core 文件:
core-gaussdb-8512-2026_07_10_11_10_40-bbox
- 进程:
/opt/cluster/usr/local/core/app/bin/gaussdb -D /opt/cluster/var/lib/engine/data1
- 主线程 PID:8512,崩溃线程 LWP:11419
- 总线程数:209
- GaussDB 版本:506.0.0.SPC0100
崩溃原因
信号:SIGSEGV (signal 11) — 空指针解引用(si_addr = 0x0)
崩溃调用栈
#0 BBOX_CreateCoredump() — bbox_create.cpp:393 (BBOX信号处理)
#1 bbox_handler(sig=11) — gs_bbox.cpp:125 (SIGSEGV handler)
#2 <signal handler called>
#3 lexical_parameterize_ssu_mutator() — analyze.cpp:6044 ★ 崩溃点 ★
#4 node_expression_tree_mutator_func() — nodefuncs_mutator.cpp:755
#5 node_expression_tree_mutator_func() — nodefuncs_mutator.cpp:374
#6 node_expression_tree_mutator_func() — nodefuncs_mutator.cpp:691
#7 node_expression_tree_mutator_func() — nodefuncs_mutator.cpp:755
#8 lexical_make_update_shared() — analyze.cpp:6115
#9 lexical_update_stmt() — analyze.cpp:6272
#10 lexical_stmt() — analyze.cpp:728
#11 lexical_top_level_stmt() — analyze.cpp:490
#12 lexical_parse_analyze_varparams() — analyze.cpp:289
#13 proc_parse_tree() — postgres.cpp:4815
#14 exec_parse_message() — postgres.cpp:5083
#15 gs_process_command() — postgres.cpp:12234
#16 PostgresMain() — postgres.cpp:11313
触发崩溃的 SQL
update test_reorder a set id =
(select s.newid from (select b.id, row_number()over (order by b.id) newid from test_reorder b) s
where a.id = s.id)
根因分析
崩溃发生在 SQL 解析/分析阶段(尚未执行),具体在 lexical_parameterize_ssu_mutator 函数中(analyze.cpp:6044)。
反汇编关键路径:
+96: call lexical_sublink_is_ssu(Node*) ; 检查是否是 SSU 子链接 → 返回 true
+105: mov 0x28(%rbx), %rdi ; 加载 SubLink->subselect
+109: mov 0x68(%rdi), %rax ; 加载 subselect 的 ssu_slink_tlist 字段
+113: test %rax, %rax ; 检查是否为 NULL
+116: je +336 ; NULL → 跳转到崩溃陷阱
+336: mov 0x0, %rax ; ★ SIGSEGV ★
+344: ud2 ; unreachable trap
关键变量
node = 0x7f95c2f23f10,类型 = T_SubLink(子查询节点)
ssu_ctx->ssu_slink_tlist = 0x0(NULL!)
slink_te = 0x0(NULL!)
referred_slink = 0x0(NULL!)
结论
这是 GaussDB 内核 BUG,位于 SSU(Shared Set Update)优化代码路径中。
UPDATE ... SET col = (correlated_subquery) 语句在解析阶段,SSU 优化器通过 lexical_sublink_is_ssu() 识别出该子链接可优化,但在 lexical_parameterize_ssu_mutator 中访问子链接的 target list(ssu_slink_tlist)时,该字段尚未初始化(为 NULL),导致空指针解引用。
涉及模块:src/compatibility/sql_adaptor/parser/analyze.cpp 中的 SSU 参数化逻辑(约第 6044 行)。
建议:
- 检查
lexical_sublink_is_ssu() 是否对含 ROW_NUMBER() 窗口函数的关联子查询误判为 SSU 可优化
- 在
lexical_parameterize_ssu_mutator 中增加对 ssu_slink_tlist 为 NULL 的防御性检查
- 可向华为提交此 BUG(影响版本 506.0.0.SPC0100)
AI 的分析确实比我的分析更加专业。我只给了一句话让 AI 去分析,权限提前都已授予,中间没有产生任何人机交互,AI 直接就分析好了。而且注意,GaussDB 目前没有公开源码,这里涉及的相关逻辑在 openGauss 中也不存在,无法参考 openGauss。(不过 GaussDB 从 507 版本开始,内核安装包里不再提供符号表,需要另外下载,非原厂人员定位问题的难度会加大。)
我又去 GaussDB 最新发布的 507.0 版本上测试了一下,发现这个问题没有复现,猜测华为已经修复了这个问题。
那么,这个问题还能继续分析下去吗?
继续深入
这里其实有个疑问:这条 SQL 里使用了分析函数 row_number(),coredump 就一定与这个函数相关吗?我们可能会默认这属于普通函数场景,可能不认为它有 BUG,然后就没继续分析了。
不过我想通过 507 版本看看这个问题是怎么修复的,结果发现 507 版本显示的执行计划根本不走这个子链接提升的优化路径,仍然保持了 SubPlan:
Update on test_reorder a (cost=0.00..1521.83 rows=91 width=874)
-> Seq Scan on test_reorder a (cost=0.00..1521.83 rows=91 width=874)
SubPlan 1
-> Subquery Scan on s (cost=13.87..16.60 rows=1 width=8)
Filter: (a.id = s.id), (Expression Flatten Optimized)
-> WindowAgg (cost=13.87..15.46 rows=91 width=32)
-> Sort (cost=13.87..14.10 rows=91 width=32)
Sort Key: b.id
-> Seq Scan on test_reorder b (cost=0.00..10.91 rows=91 width=32)
于是我回到 506 版本上,把这个 row_number() 函数改成原本的 id 字段,想看看是不是能显示出 join 的执行计划。然而数据库仍然 coredump 了,这说明这个 bug 与分析函数根本没有关系——纯粹是因为 update 里进行了子查询提升!
update test_reorder a set id =
(select s.newid from (select b.id, b.id newid from test_reorder b) s
where a.id = s.id);
我让 AI 分析了一下这个新的 core:
Copilot 分析报告:
这个 core dump 与 192.168.163.118 上的完全相同。
────────────────────
GaussDB Core Dump 分析报告(192.168.163.119)
- Core 文件:
core-gaussdb-399212-2026_07_10_16_36_48-bbox
- 路径:
/opt/cluster/var/lib/engine/data1/data/dn_6001/
- 崩溃线程:LWP 399912
- 信号:SIGSEGV (signal 11),
si_addr = 0x0(空指针解引用)
- GaussDB 版本:506.0.0.SPC0100(同一 BuildID:
4c19e2530a5eaf287a9e68ede890dd06)
触发 SQL
update test_reorder a set id =
(select s.newid from (select b.id, b.id newid from test_reorder b) s
where a.id = s.id)
崩溃调用栈
#3 lexical_parameterize_ssu_mutator() — analyze.cpp:6044 ★ 崩溃点 ★
#4 node_expression_tree_mutator_func() — nodefuncs_mutator.cpp:691
#5 node_expression_tree_mutator_func() — nodefuncs_mutator.cpp:755
#6 lexical_make_update_shared() — analyze.cpp:6115
#7 lexical_update_stmt() — analyze.cpp:6272
#8 lexical_stmt() — analyze.cpp:728
#9 lexical_top_level_stmt() — analyze.cpp:490
#10 lexical_parse_analyze_varparams() — analyze.cpp:289
#11 proc_parse_tree() — postgres.cpp:4815
#12 exec_parse_message() — postgres.cpp:5083
关键变量(与 118 节点完全一致)
| 变量 |
值 |
说明 |
| node |
0x7fcf70015b10 |
类型 = T_SubLink |
| ssu_ctx->ssu_slink_tlist |
0x0 (NULL) |
SSU 子链接目标列表未初始化 |
| slink_te |
0x0 (NULL) |
子链接目标条目为空 |
| referred_slink |
0x0 (NULL) |
引用的子链接为空 |
| si.si_addr |
0x0 |
访问地址为 NULL |
结论
与 192.168.163.118 上的 core dump 是同一个 BUG,崩溃位置、根因、调用栈完全一致:
- 同一函数:
lexical_parameterize_ssu_mutator (analyze.cpp:6044)
- 同一触发模式:
UPDATE ... SET col = (correlated subquery FROM table) WHERE correlated_condition
- 同一原因:SSU 优化器误判关联子链接可优化,但
ssu_slink_tlist 为 NULL 导致空指针解引用
两台机器的 SQL 区别仅在于子查询:
- 118 节点:
row_number()over (order by b.id) newid(窗口函数)
- 119 节点:
b.id newid(简单列引用)
说明此 BUG 不依赖窗口函数,任何 UPDATE SET col = (关联子查询) 的写法都可能触发。
建议
- 紧急规避:避免使用
UPDATE ... SET col = (correlated subquery) 语法,改用 JOIN 或 CTE 重写
- 代码修复:在
lexical_parameterize_ssu_mutator (analyze.cpp:6044) 中对 ssu_slink_tlist 增加 NULL 检查
- 排查
lexical_sublink_is_ssu():该函数对关联子查询的 SSU 可优化判断可能存在逻辑缺陷
最终结论就是:update 的 set 值来自一个嵌套两层的子查询(两个 select)时,就会触发 coredump。
总结与思考
一句话总结:
在 GaussDB 506.0.0SPC0100/SPC0500 版本上,执行形如 update set (select ... from (select ... from )) 的 SQL 会导致数据库 coredump,该问题已在 GaussDB 507 版本修复。
思考:
本次发现的这个问题,我差点就由于 AI 偷懒误判了问题的严重性——认为与 update 子查询里套窗口函数相关,但实际上与窗口函数没有关系。尽管有了 AI 辅助,人类依然必须时刻保持清醒。以前没有 AI 的时候,我可能会挖根刨底找到问题根因,而不是停留在窗口函数这个表象。有了 AI 之后我也变懒了,AI 自己也偷懒,问题就会被误判。幸好我仍然保留了一部分深入探究和合理质疑的习惯,没有在 AI 第一次分析后就认可 AI 给的结论。当然,使用能力更强的模型或者通过提示词加强约束,可能 AI 更不容易偷懒,但那也只是“更不容易”而已。
2026-07-15 更新
今天在看 GaussDB 507 版本的更新说明时,发现有一个 enable_shared_set_update 参数,让我联想到了本文分析时发现的内核 SSU 特性,就顺便到 506 版本上看了下这个参数的说明:
enable_shared_set_update
参数说明:对于 UPDATE 语句中使用子查询更新多个列的场景,该参数可以控制优化器是否对每个待更新列分别生成一个子计划。当打开该参数时,将只生成一个子计划在多个列之间共享。
参数类型:布尔型
参数单位:无
取值范围:
- on:表示仅根据子查询生成一个子计划
- off:表示为每个待更新列分别生成一个子计划
默认值:on
设置方式:该参数属于 USERSET 类型参数,请参见表1中对应设置方法进行设置。
设置建议:推荐使用默认值。
设置不当的风险与影响:关闭可能会降低使用子查询更新多个列的查询执行效率,且增加查询优化和执行开销。
我尝试在 506 版本上关闭 enable_shared_set_update 参数,果然本文的用例就不再 coredump 了。但关闭这个功能后,对于使用子查询 update 多列的情况,性能会按照更新的列数成倍数下降。
下面是在 507 版本上开关 enable_shared_set_update 的对比,重点关注 SubPlan 的个数和 Output 的内容:
开启(on):
gaussdb=> select version();
gaussdb (GaussDB Kernel 507.0.0 build 19fa72ae) compiled at 2026-05-31 13:54:32 last mr 27222 release
(1 row)
gaussdb=> set explain_perf_mode to normal;
SET
gaussdb=> show enable_shared_set_update;
enable_shared_set_update
--------------------------
on
(1 row)
gaussdb=> explain performance
update test_reorder a set (id, c1) =
(select s.newid, s.id from (select b.id, b.id newid from test_reorder b) s
where a.id = s.id);
QUERY PLAN
------------------------------------------------------------------------------------------------------
Update on admin.test_reorder a (cost=0.00..1024.88 rows=91 width=456) (actual time=0.205..0.753 rows=10 loops=1)
(Buffers: shared hit=112 dirtied=3)
(CPU: ex c/r=114087, ex row=10, ex cyc=1140870, inc cyc=2639148)
-> Seq Scan on admin.test_reorder a (cost=0.00..1024.88 rows=91 width=456) (actual time=0.063..0.428 rows=10 loops=1)
Output: $1, $2, a.c2, (SubPlan 1), a.ctid, (Expression Flatten Optimized)
(Buffers: shared hit=101)
(CPU: ex c/r=149827, ex row=10, ex cyc=1498278, inc cyc=1498278)
SubPlan 1
-> Seq Scan on admin.test_reorder b (cost=0.00..11.14 rows=1 width=32) (actual time=0.298..0.324 rows=10 loops=10)
Output: b.id, b.id, (Expression Flatten Optimized)
Filter: (a.id = b.id), (Expression Flatten Optimized)
Rows Removed by Filter: 90
(Buffers: shared hit=100)
(CPU: ex c/r=11255, ex row=100, ex cyc=1125518, inc cyc=1125518)
Total runtime: 0.981 ms
(15 rows)
关闭(off):
gaussdb=> set enable_shared_set_update to off;
SET
gaussdb=> explain performance
update test_reorder a set (id, c1) =
(select s.newid, s.id from (select b.id, b.id newid from test_reorder b) s
where a.id = s.id);
QUERY PLAN
------------------------------------------------------------------------------------------------------
Update on admin.test_reorder a (cost=0.00..2038.39 rows=91 width=456) (actual time=0.205..1.195 rows=10 loops=1)
(Buffers: shared hit=212 dirtied=3)
(CPU: ex c/r=126086, ex row=10, ex cyc=1260860, inc cyc=4187080)
-> Seq Scan on admin.test_reorder a (cost=0.00..2038.39 rows=91 width=456) (actual time=0.066..0.836 rows=10 loops=1)
Output: (SubPlan 1), (SubPlan 2), a.c2, a.ctid, (Expression Flatten Optimized)
(Buffers: shared hit=201)
(CPU: ex c/r=292622, ex row=10, ex cyc=2926220, inc cyc=2926220)
SubPlan 1
-> Seq Scan on admin.test_reorder b (cost=0.00..11.14 rows=1 width=32) (actual time=0.328..0.367 rows=10 loops=10)
Output: b.id, (Expression Flatten Optimized)
Filter: (a.id = b.id), (Expression Flatten Optimized)
Rows Removed by Filter: 90
(Buffers: shared hit=100)
(CPU: ex c/r=12884, ex row=100, ex cyc=1288424, inc cyc=1288424)
SubPlan 2
-> Seq Scan on admin.test_reorder b (cost=0.00..11.14 rows=1 width=32) (actual time=0.280..0.305 rows=10 loops=10)
Output: b.id, (Expression Flatten Optimized)
Filter: (a.id = b.id), (Expression Flatten Optimized)
Rows Removed by Filter: 90
(Buffers: shared hit=100)
(CPU: ex c/r=10563, ex row=100, ex cyc=1056332, inc cyc=1056332)
Total runtime: 1.438 ms
(22 rows)
子查询 update 多列时生成多个 SubPlan 导致性能变差,这个问题需要拆开看:一是支持使用子查询 update 多列,二是生成合理的执行计划。第一点,原生 PostgreSQL 是 9.5 版本加入的,但当时 PG 实现这个功能时就只有一个 SubPlan,更新多个字段时 SubPlan 里一次性返回多个字段,也就是说原生 PG 从一开始支持子查询更新多列时就不存在这个性能问题。GaussDB 是从 PostgreSQL 9.2.4 开始分叉的,所以 GaussDB 从一开始就是华为自研的子查询 update 多列,没有用原生 PG 的方案。只是不确定是不是当时研发偷懒了,直接一个递归,每个字段都单独重新查询,方便适配原本的单字段更新逻辑。这个点我在前几年做其他 OG 系国产数据库的内核需求分析时也遇到过,倒是没想到 GaussDB 已经做过优化了。