文章目录那天下午我差点掀桌子先搞明白标量子查询咋就惹了众怒你以为改成JOIN就完了天真KES那套小心翼翼的手术流程等价性判定——先问能不能动改写外连接——动了怎么缝合并相似子查询——能省则省优化器在思考这件事数据不会撒谎用起来还是有些门道不是子查询消除这么简单兼容是对前人努力的尊重是确保业务平稳过渡的基石然而这仅仅是故事的起点那天下午我差点掀桌子说真的到现在我都记得客户现场报表SQL跑了三十多秒出不来结果业务那边一个电话接一个电话催。我扒拉执行计划看了半天SELECT后面挂了四五个标量子查询全是对同一张表反复扫。当时心里就骂了——这玩意儿要是能自动把子查询变成JOIN多好省得我每次手动改。后来KES V009R002C014出了标量子查询消除我第一时间拿来测。t1和t2各一万行那个原来32秒的SQL直接干到了24毫秒。24毫秒啊各位我当时盯着屏幕愣了好几秒以为测错了重跑了一遍还是24毫秒。一千多倍这不是什么优化不优化的事了这是从人等机器变成了机器等人。但你别急着开香槟。这个功能背后的水比你想的深。今天我掰开了揉碎了聊聊——为啥标量子查询是个坑为啥不能随手一改还有KES的优化器到底是怎么像个人一样在做判断的。嗯…不完全是像个人更像是那种特别谨慎的法医不动则已一动必准。先搞明白标量子查询咋就惹了众怒标量子查询就那种只返回一行一列的子查询放在SELECT列表里当值用。写起来确实舒服业务开发特别爱——一个括号套一个聚合查询逻辑一目了然维护也方便。SELECTt1.id,(SELECTSUM(amount)FROMt2WHEREt2.ref_idt1.id)AStotal_amount,(SELECTCOUNT(*)FROMt2WHEREt2.ref_idt1.id)ASitem_count,(SELECTAVG(price)FROMt2WHEREt2.ref_idt1.id)ASavg_priceFROMt1;看着清清爽爽对吧。但跑起来你就知道了——t1每吐一行出来三个子查询挨个执行一遍t2就被扫了又扫扫了又扫。t1一万行的话t2被扫三万次就算每次走索引三万次B树从根到叶子的遍历CPU那叫一个遭罪。我之前碰到过一个更离谱的统计模块SELECT后面挂了十几个标量子查询那报表跑了两分多钟才出来。后来DBA手动改成LEFT JOIN三秒出结果但改完出事了——COUNT那块空值处理没搞对数据对不上又回滚了。这个坑我后面细说。子查询的执行模型就这么个德性行式触发一行一启动。本质上就是嵌套循环时间复杂度是O(外层行数 × 子查询代价)。数据少的时候你不觉得数据一上去……嗯那就等着被业务方喷吧。你以为改成JOIN就完了天真我手动改写过不少次标量子查询确实每次改完都快了但改的时候永远提心吊胆。这里面的坑真的我踩过的可能比你见过的都多。先说多行返回那个事。标量子查询有个隐藏的约束运行时最多返回一行多了就直接报错scalar subquery returned more than one row。这个报错其实是个安全阀提醒你关联条件不唯一或者缺聚合。但你把它改成LEFT JOIN之后呢原来该报错的地方不报了而且一行可能膨胀成多行结果集直接变脏了。-- 这个如果t2里ref_id有重复会报错SELECT(SELECTamountFROMt2WHEREt2.ref_idt1.id)FROMt1;-- 改成LEFT JOIN报错没了行可能膨胀你还不一定看得出来SELECTt2.amountFROMt1LEFTJOINt2ONt1.idt2.ref_id;生产环境里这种问题简直是要命的——数据悄摸就错了你还不知道。然后是COUNT那个空值陷阱。这个我印象太深了我踩的那次折腾了一整个下午才定位到。事情是这样的标量子查询没匹配到数据时SUM/AVG/MAX/MIN返回NULL但COUNT返回0。你用LEFT JOIN改写之后呢没匹配的时候右表全是NULLCOUNT也变成NULL了——但原子查询应该返回0啊。-- 原始写法没匹配cnt0, sum_amtNULLSELECTt1.id,(SELECTCOUNT(*)FROMt2WHEREt2.ref_idt1.id)AScnt,(SELECTSUM(amount)FROMt2WHEREt2.ref_idt1.id)ASsum_amtFROMt1;-- 不加COALESCE的改写cnt变NULL了错了SELECTt1.id,agg.cnt,agg.sum_amtFROMt1LEFTJOIN(SELECTref_id,COUNT(*)AScnt,SUM(amount)ASsum_amtFROMt2GROUPBYref_id)aggONt1.idagg.ref_id;所以COUNT类的子查询必须用COALESCE把NULL补回0SUM那些就不用。优化器得挨个记下每个聚合函数的类型然后分别处理。说起来就一行代码的事真做起来特别容易漏我踩过这个坑所以知道。还有GROUP BY的问题。如果子查询里写了GROUP BY但分组键不是唯一的子查询本身可能返回多行这种你消除了之后分组键不唯一嘛一行变多行。另外DISTINCT和LIMIT也是禁区——DISTINCT的语义消除后去重逻辑变了不好保证等价LIMIT的话改写后行的顺序数量都不好控制。碰到这些优化器只能绕着走。所以你想想能不能消除这个判定本身就很复杂哪是简单的模式匹配能搞定的。优化器得跟法医验尸似的把子查询每个细节翻个底朝天确认消除后结果绝对不会变才敢下刀。KES那套小心翼翼的手术流程KES在V009R002C014里正式上线了标量子查询消除。整体就三步——先判等价性再改写外连接最后合并相似的。听着简单每一步都是细活。等价性判定——先问能不能动这是整个机制最核心也最难的一步。KES的思路我挺认同的——不是尽可能多地消除而是只消除绝对安全的。保守吗保守。但数据库优化器最怕的从来不是慢是结果不对。宁可少优化一个也不能把结果搞错这个工程哲学我服。判定的时候主要查这些东西子查询位置目前只处理SELECT列表里的WHERE和HAVING那边的关联条件跟外层各种交叉引用太复杂暂不碰子查询结构聚合函数、GROUP BY、WHERE、JOIN可以有但窗口函数、UNION、DISTINCT、LIMIT/OFFSET这些结构太复杂等价性保证不了就不动标量语义验证有GROUP BY的检查分组键唯一性没聚合的检查关联列唯一约束COUNT空值处理后面生成COALESCE的时候按类型来相关条件提取只支持简单等值条件复杂表达式目前搞不定。-- 开启标量子查询消除SETkdb_rbo.rbo_ruleon;SETkdb_rbo.enable_scalar_subquery_removalon;哎等一下有个细节必须提。rbo_rule这个参数可以设成cost让优化器根据代价估算自己决定要不要消除。这就不是看到就消了而是算了代价再消——从RBO往CBO迈了一步。我当时看到这个参数设置的时候还挺惊喜的说明KES在设计的时候就想到了不是所有场景消除都划算。这个后面我再展开聊。改写外连接——动了怎么缝过了安全校验KES就把标量子查询提出来变成内联视图跟外表做LEFT JOIN。用LEFT JOIN不用INNER JOIN是有讲究的——标量子查询即使没匹配也不会减少主查询的结果行数LEFT JOIN刚好保住这一点。-- 原来的SELECT(SELECTSUM(id)FROMt2WHEREt1.idt2.id)FROMt1;-- KES内部给你改成这样SELECTCOALESCE(temp.sum_id,0)FROMt1LEFTJOIN(SELECTid,SUM(id)ASsum_idFROMt2GROUPBYid)tempONt1.idtemp.id;改完之后t2只扫一次做GROUP BY然后跟t1做连接。逐行触发的嵌套循环变成了集合操作性能飞跃的根本就在这。而且改写完之后优化器还能在连接上继续折腾——Hash Join还是Merge Join用不用索引这些在子查询场景下想都别想的策略现在全都能用了。相当于打开了新的搜索空间。合并相似子查询——能省则省SELECT后面要是挂了好几个结构差不多的子查询KES会自动把它们合成一个内联视图。合并条件挺严的——同一张基表、同一个连接条件、同一个GROUP BY键、同一个本地过滤条件缺一不可。-- 原来两个差不多的子查询SELECTs11.id1,(SELECTSUM(s22.id1)FROMs22WHEREs22.id3s11.id3),(SELECTSUM(s22.id2)FROMs22WHEREs22.id3s11.id3)FROMs11;-- KES给你合并了SELECTs11.id1,COALESCE(agg.sum_id1,0),COALESCE(agg.sum_id2,0)FROMs11LEFTJOIN(SELECTid3,SUM(id1)ASsum_id1,SUM(id2)ASsum_id2FROMs22GROUPBYid3)aggONs11.id3agg.id3;原来两个子查询各扫一次s22合并后就一次。那种SELECT后面挂了一堆同构子查询的报表SQL这步真的是救命的。我之前碰到过一个二十多个标量子查询全查同一张事实表的合并之后事实表就扫一次……你想想二十次和一次的差距。优化器在思考这件事聊到这儿我想扯远一点。因为KES标量子查询消除的设计跟一个更大的话题有关系——优化器正在从机械翻译变成智能决策。数据库优化器的演进教科书告诉你三个阶段。最早RBO基于规则看到模式就改写不管代价。然后CBO基于代价枚举候选计划算代价选最便宜的。再往后ABO基于AI用机器学习搞基数估计和计划选择。但实际哪有这么泾渭分明。你看KES这个标量子查询消除——你说它算RBO还是CBO表面是逻辑优化规则应该归RBO吧但rbo_rule设成cost之后优化器会看代价来决定消不消除这分明是CBO的套路。而且等价性判定里面那些检查——分组键唯一不唯一、关联列有没有约束、统计信息靠不靠谱——这不单纯是规则匹配了这就是在做推理。学术界这边子查询优化的研究脉络说实话挺乱的。早年关注的是嵌套循环展开Unnesting后来Apply Removal的概念出来把嵌套循环形式化了再往后向量化执行时代大家关注批处理适配现在又在搞学习型优化器。但概念界定就很模糊——有人把子查询消除归查询重写有人归逻辑优化还有人单列一个解关联阶段。这种界定混乱本身就说明了子查询优化不是一个孤立的技术点是一整条决策链。我翻了不少论文有个观察觉得挺值得说。学习型优化器现在分两大路子端到端的直接用神经网络生成执行计划和助手型的AI辅助传统优化器的某个环节。端到端听着炫酷但落地问题一堆——训练数据哪来没见过的SQL怎么办泛化性怎么保证相比之下助手路线更务实些。openGauss的ABO用了贝叶斯网络做多列基数估计选择率估算准了不少。但也有人批评说贝叶斯网络对数据分布的假设太强了如果你的数据根本不满足条件独立性搞不好比传统方法还差。而且训练贝叶斯网络本身就要时间ANALYZE的时候多等那几分钟你能不能接受DB2那边2026年在搞Self-Correcting Optimizer用强化学习做反馈循环——跑完看实际代价跟估算差多少反过来调代价模型。方向没问题但强化学习冷启动是大问题刚开始可能还不如传统CBO。OceanBase跟华东师大搞的APQO框架在SIGMOD 2026发了论文离线训练在线校准据说查询延迟降40%到60%。但这玩意儿需要历史负载做训练全新部署的系统用不了。说实话我觉得这些学术研究跟工程实践之间还是有挺大鸿沟的。论文里跑的分往往是在特定数据集特定负载下的结果换到真实业务场景里表现怎样很难说。而且大部分研究只关注了优化效果对优化器本身的编译时间开销、内存占用这些几乎不提。但这在生产环境里是绕不开的问题。说回KES。它的标量子查询消除我觉得已经体现出了推理式决策的雏形。你看优化器在决定要不要消除的时候——这个结构安不安全消除后语义变不变统计信息能不能撑住代价估算相似子查询能不能合并合并完代价是涨还是跌这一连串决策跟AI推理的逻辑本质上是一回事在多种可能的选择中根据已知信息做最优判断。它不是那种看到子查询就无脑翻译成JOIN的机械工具是一个在复杂条件下做审慎决策的……嗯我愿意叫它智能决策大脑。当然离真正的学习型优化器还远着呢目前KES的推理还是基于预定义规则和代价模型不是从执行反馈中学习。但rbo_rulecost这个设计让优化器学会了三思而后行这本身就是从RBO到CBO的关键一步。数据不会撒谎我自己的测试环境跑的t1和t2不同数据量数据量消除前消除后1000行3.2秒0.003秒5000行16.1秒0.012秒10000行32.4秒0.024秒20000行65.2秒0.048秒三个数量级没开玩笑。数据量越大差距越离谱——标量子查询的代价是线性的翻倍就翻倍消除后JOIN的代价增长就平缓多了。还有并发数据100并发TPS提了大约60%响应时间缩短约90%。对在线业务来说这基本上就是从不可用到可用的区别了。-- 想自己试试的话CREATETABLEt1(aINT,bINT);CREATETABLEt2(aINT,bINT);INSERTINTOt1VALUES(generate_series(1,10000),generate_series(1,10000));INSERTINTOt2VALUES(generate_series(1,10000),generate_series(1,10000));-- 消除前看执行计划EXPLAIN(costsoff)SELECT(SELECTMAX(b)FROMt2WHEREt2.at1.a)FROMt1;-- 开启消除SETkdb_rbo.rbo_ruleon;SETkdb_rbo.enable_scalar_subquery_removalon;-- 再看SubPlan消失了变成Hash JoinEXPLAIN(costsoff)SELECT(SELECTMAX(b)FROMt2WHEREt2.at1.a)FROMt1;执行计划从SubPlan嵌套循环变成Hash Join或Merge Joint2就扫一次。自己跑跑看就知道了差别肉眼可见。用起来还是有些门道效果是炸裂的但实际落地上有些事得注意。这个功能默认关闭的。得手动设kdb_rbo.rbo_rule和kdb_rbo.enable_scalar_subquery_removal。session级别SET一下就行确认没问题再写进kingbase.conf全局生效。别上来就全局开万一出了事不好回滚。-- 先session级别试SETkdb_rbo.rbo_ruleon;SETkdb_rbo.enable_scalar_subquery_removalon;-- 确认ok了再写配置-- kingbase.conf加-- kdb_rbo.rbo_rule on-- kdb_rbo.enable_scalar_subquery_removal on不是所有子查询都能消。窗口函数、UNION、DISTINCT、LIMIT/OFFSET这些结构的等价性保证不了优化器碰到就跳过走原始路径。这不是偷懒是谨慎。相关条件只支持简单等值。像ABS(oi.order_id) o.order_id这种带函数的关联条件GROUP BY键不好定——到底按oi.order_id分还是按ABS(oi.order_id)分这个等价推理更复杂KES暂时还没做到。合并条件也比较严。同基表同连接条件同GROUP BY键同过滤条件四条全满足才行。两个子查询WHERE条件差一点一个typeA’一个type‘B’就不合并。后续版本可能会放开。我还碰到过一次比较隐蔽的——消除之后反而变慢了。数据分布比较特殊LEFT JOIN中间结果集太大还不如原来走索引的嵌套循环。所以rbo_rulecost真的很重要让优化器根据代价来定比一刀切靠谱。不是子查询消除这么简单写到最后我想说KES这个标量子查询消除看着是个优化规则但它折射出来的东西比这大得多。2026年了AI重构软件栈已经是大势。数据库优化器从RBO到CBO到ABO的演进方向没跑了。但你仔细看KES标量子查询消除——等价性判定阶段检查唯一约束、分析分组键、处理COUNT空值语义、评估消除代价——这些不就是AI推理在做的事吗在多条决策路径中根据结构信息和统计信息选最优的那条。它是个智能决策大脑不是个机械翻译器。当然离学习型优化器还有距离目前推理还是基于预定义规则和代价模型不是从执行反馈中学习。但方向对了。rbo_rulecost这个设计让优化器学会了三思而后行从RBO到CBO的关键一步已经迈出去了。我有个不太成熟的想法吧——未来ABO优化器说不定会把标量子查询消除的决策也纳入学习范围。根据历史执行数据学什么特征的子查询消除后收益大什么特征的消了反而拉胯。到那时候优化器就不只是在推理了是在进化。不过这是后话。眼下KES标量子查询消除已经够实用了。32秒到24毫秒的数据摆着100并发TPS提60%摆着。你业务SQL里要是有大量标量子查询赶紧开起来试试。记得先用rbo_rulecost让优化器自己判断别一上来就强制on。