【GaussDB】分析一个使用子查询进行update导致的coredump
背景
隔壁驻场同事在帮客户做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 ,即解析SQL,所以应该是可以稳定复现的。再往里层看,是做参数化路径处理时出了问题。
尝试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
#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() 识别出该子链接是 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
分析完成。这个 core dump 与 192.168.163.118 上的是 完全相同的 BUG。
────────────────────
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)崩溃调用栈
#0 BBOX_CreateCoredump() — bbox_create.cpp:393
#1 bbox_handler(sig=11) — gs_bbox.cpp:125
#2
#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
#13 gs_process_command() — postgres.cpp:12234
#14 PostgresMain() — postgres.cpp:11313关键变量(与 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
总结与思考
一句话总结:
在 GaussDB506.0.0SPC0100/SPC0500 版本上,执行形如 update set (select ... from (select ... from )) 的SQL会导致数据库coredump,该问题已在GaussDB507版本修复。
思考:
本次发现的这个问题,我差点就由于AI偷懒误判了问题的严重性,认为与update子查询里套窗口函数相关,但是实际上与窗口函数没关系。尽管有了AI辅助,人类仍然必须时刻保持清醒。以前没有AI的时候,我可能会挖根刨底,找到问题根因,而不是停在窗口函数这个表象,有了AI我也懒了,AI自己也懒,问题就会被误判。幸好我仍然保留了一部分深入探究和合理质疑的习惯,没有在AI第一次分析后就认可AI给的结论。当然使用其他能力更强的模型,或者通过提示词加强约束,可能AI更不容易偷懒,但是那也只是“更不容易”而已。
20260715更新
今天在看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多列的情况,性能就会按更新的列数成倍数下降。
下面是开关enable_shared_set_update的对比,关注subplan的个数和Output的内容
gaussdb=> select version();
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)
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)
gaussdb=>
子查询update多列生成多个subplan导致性能差,这个问题要拆开看,一是支持使用子查询update多列,二是生成合理的执行计划。第一个点,原生PG是9.5版本加的,但是当时PG实现这个功能的时候就只有一个subplan,更新多个字段时,subplan里是一次性返回了多个字段,也就是原生PG从一开始支持使用子查询更新多列时就不存在性能问题。GaussDB是从PG9.2.4开始分叉,所以GaussDB从一开始就是华为自研的子查询update多列,没有用原生PG的方案。只是不确定是不是当时研发偷懒了,直接一个递归,每个字段都单独重新查询,方便适配原本的单字段更新逻辑。这个点我在前几年做其他OG系国产数据库的内核需求分析的时候也遇到过,倒是没想到GaussDB已经做过优化了。

