目 录CONTENT

文章目录

【GaussDB】分析一个使用子查询进行update导致的coredump

DarkAthena
2026-07-18 / 0 评论 / 0 点赞 / 2 阅读 / 0 字

【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 行)

建议:

  1. 检查  lexical_sublink_is_ssu()  是否对含  ROW_NUMBER()  窗口函数的关联子查询误判为 SSU 可优化
  2. 在  lexical_parameterize_ssu_mutator  中增加对  ssu_slink_tlist  为 NULL 的防御性检查
  3. 可向华为提交此 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 = (关联子查询)  的写法都可能触发。

建议

  1. 紧急规避:避免使用  UPDATE ... SET col = (correlated subquery)  语法,改用 JOIN 或 CTE 重写
  2. 代码修复:在  lexical_parameterize_ssu_mutator  (analyze.cpp:6044) 中对  ssu_slink_tlist  增加 NULL 检查
  3. 排查  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已经做过优化了。

0
  1. 支付宝打赏

    qrcode alipay
  2. 微信打赏

    qrcode weixin
博主关闭了所有页面的评论