找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

4322

积分

0

好友

560

主题
发表于 1 小时前 | 查看: 4| 回复: 0

本文首发时间 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 行)。

建议:

  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:

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 = (关联子查询) 的写法都可能触发。

建议

  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。

总结与思考

一句话总结

在 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 已经做过优化了。





上一篇:智谱GLM‑5.3正式发布:编程能力大幅跃升,后训练Scaling立功
下一篇:GaussDB高可用切换数据库卡顿:自治事务并发创建超时排查
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-8-17 04:40 , Processed in 1.241492 second(s), 39 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表