国产化落地避坑 · 干货向|Oracle 迁金仓 KES,我把外连接消除排在隐性陷阱第一位

从 Oracle 迁到金仓 KES 这几年,我攒了一份隐性陷阱清单。所谓隐性,是它们不报错。语法过了,程序跑了,数据也出来了,只是结果悄悄和 Oracle 不一样。这类坑比报错的坑难缠十倍,因为报错会拦住你,它不会,它让你带着错误的数据一路上线。

这份清单里,我把外连接消除排在第一位。

原因很简单,它同时踩中了三件最要命的事,静默、常见、跟数据正确性直接挂钩。一条你写了很多年的 LEFT JOIN,迁过来行数就少了一截,业务方在群里问上周的数据怎么对不上,你回去翻代码,SQL 一个字没改,放回 Oracle 上跑还是对的。

先说清楚它长什么样。

一、先复现,LEFT JOIN 的行数为什么少了

假设有两张表,t1 是左表,也就是驱动表,t2 是右表。需求是查出 t1 的所有记录,同时把 t2 里 name2 为 'cc' 的信息带出来。很多人会顺手写成这样。

sql 复制代码
SELECT * FROM t1 LEFT JOIN t2 ON t1.id1 = t2.id2
WHERE t2.name2 = 'cc';

按 LEFT JOIN 的直觉,你预期的结果是,t1 的所有行都在,t2 匹配上且 name2 为 'cc' 的显示数据,匹配不上的显示 NULL。

实际拿到的结果是,只剩下 t1 与 t2 成功匹配、并且 t2.name2 为 'cc' 的那些行。t1 里没匹配上的记录,全没了。打开执行计划,你会看到原本写的 Outer Join,被优化器换成了 Inner Join。

这就是外连接消除。你的 LEFT JOIN 在执行计划层面被降级成了 INNER JOIN。

二、优化器为什么敢把外连接改成内连接

这不是 bug,是优化器按 SQL 语义做的一次合法变换。想明白它,抓住两点就够。

第一点,WHERE 的过滤发生在 JOIN 之后。LEFT JOIN 先执行,右表没匹配上的行,t2 那一侧的列会被填成 NULL。然后 WHERE 才上场。你的条件是 t2.name2 = 'cc',而对那些填了 NULL 的行来说,NULL = 'cc' 的结果不是 false,是 Unknown,在 WHERE 里 Unknown 一样会被过滤掉。于是外连接辛苦保留下来的那些 NULL 行,被 WHERE 一句话全删了。

第二点,优化器会做等价变换检查。它发现,既然 WHERE 里这个针对右表非空列的条件,注定会把外连接产生的所有 NULL 行过滤干净,那么「外连接加这个过滤」的最终结果,跟「内连接加这个过滤」在数学上完全一样。两条路终点相同,优化器当然挑代价更低的那条,也就是内连接。

所以它不是算错了,是你写的这条 SQL 在语义上本来就等价于一条内连接,优化器只是把这层等价关系用了起来。真正的问题在于,你以为你在写外连接,落到语义上却给了它一条内连接。

三、有一种情况它不会消除,IS NULL

不是所有针对右表的条件都会触发消除。最典型的例外是 IS NULL。

sql 复制代码
SELECT * FROM t1 LEFT JOIN t2 ON t1.id1 = t2.id2
WHERE t2.name2 IS NULL;

这条不会被消除,道理也顺。外连接的核心用途之一,就是找出右表里缺失的记录,而 t2.name2 IS NULL 恰恰是在捞这些由外连接产生的 NULL 行。这时候要是还转成内连接,那些缺失记录就永远进不了结果集,结果直接错。所以为了保证结果正确,优化器在这种场景下不会做外连接消除。

给你一个一秒判断的诀窍,看你的 WHERE 到底是在排除右表的 NULL 行,还是在专门捞右表的 NULL 行。前者会触发消除,后者不会。

四、迁移时怎么写才对

原理懂了,解法就清楚了,核心就一句话,针对右表的过滤,除非你是要查空,否则应该放进 ON,而不是 WHERE。

把过滤条件下推到 ON 子句,这是正确写法。

sql 复制代码
SELECT * FROM t1 LEFT JOIN t2 ON t1.id1 = t2.id2 AND t2.name2 = 'cc';

这样写,系统会先按 name2 = 'cc' 过滤 t2,再拿过滤后的 t2 去和 t1 做外连接。t1 的所有记录都能返回,匹配不上的那部分,t2 的列照样是 NULL。这才是你最初想要的语义。

记住这条分工,ON 控制的是连接的规则,WHERE 控制的是最终结果的筛选。这句话在整个迁移期间值得天天念。

尤其要小心 Oracle 的 (+) 语法,这条对迁移最关键。

KES 兼容 Oracle 的 (+) 外连接写法,这对迁移是好事,但同一个坑也跟着来了。从 Oracle 过来的人手里带着 (+) 的老习惯,最容易在这栽。规则是这样,如果 (+) 写在 WHERE 里,而同一个 WHERE 里的过滤条件没带 (+),一样会触发外连接消除,和前面 LEFT JOIN 那种情况一模一样。反过来,如果过滤条件也带上 (+),语义就等同于把条件放进了 ON,外连接不会被消除。

所以迁移老 Oracle 语句的时候,凡是带 (+) 的都要一条条看清楚,(+) 有没有覆盖到过滤条件,这直接决定了你的外连接活不活得下来。

作用在左表的条件不用担心 。如果过滤条件落在非空侧,也就是左表,比如 WHERE t1.name1 = 'a',这属于正常的业务过滤,意思是只对满足条件的 t1 记录做外连接,不满足的直接丢掉,它不改变连接的性质,符合预期。要留意的是另一种写法,如果你把左表条件放进 ON,那 t1 的所有数据仍然会全部返回,只是不满足条件的行不参与连接而已。这两者语义不同,迁移时别混。

五、落地排查清单

把这个坑落到具体的迁移动作上,给你三条能直接执行的。

第一,审执行计划。审涉及 OUTER JOIN 的慢 SQL、或者结果存疑的 SQL 时,重点看计划。如果你定义的 Left Join,在计划里显示成了普通的 Hash Join 或者 Nested Loop,而不是 Left 语义的连接,同时结果集行数比预期少,那多半就是发生了非预期的外连接消除。

第二,校语义。心里立一条规矩,针对右表这种可空侧的过滤,除了查空的 IS NULL,绝大多数都应该放进 ON。ON 管连接规则,WHERE 管最终筛选,这两句是迁移期的口头禅。

第三,一致性优先。外连接消除本身是个好优化,性能是它的功劳,平时求之不得。但在迁移场景里,第一优先级不是性能,是跟原系统逻辑对齐。任何优化器行为差异,只要可能让业务数据和 Oracle 对不上,都先按一致性处理,性能的事往后放。

六、为什么它排第一

回到开头那个问题,为什么我把外连接消除排在 Oracle 迁 KES 隐性陷阱的第一位。

因为它是静默这一类坑的代表。它不报错,不中断,语法完全合法,连优化器都没做错,错的只是你以为的语义、和 SQL 真实语义之间那道看不见的缝。这种坑,测试用例只要覆盖不全就一定漏,往往要等上线之后业务方拿真实数据帮你发现,代价最大。

迁移这件事,越往后走我越信一条,让系统跑起来不难,难的是让它跑出跟从前一模一样的结果。外连接消除,是这条路上的第一课。

这份隐性陷阱清单还没写完。空串和 NULL 的区别、隐式类型转换、日期格式、分页语法,每一个都够单开一篇。后面一篇一篇慢慢聊。

相关推荐
老纪的技术唠嗑局5 小时前
花一年时间整理出的:AI 数据库混合搜索入门实践
数据库
川石课堂软件测试6 小时前
安全测试|常见SQL注入攻击方式、实例及预防
服务器·数据库·sql·功能测试·测试工具·安全·单元测试
笃行3506 小时前
国产化落地避坑 · 干货向|迁移后那条「测试能过、一上生产就查空」的 SQL,多半栽在 WHERE 函数顺序上
数据库
蓝田~7 小时前
MySQL慢查询怎么优化?B+Tree索引原理+MVCC读不阻塞写+EXPLAIN执行计划,从5秒到0.05秒
数据库·mysql
sunxunyong7 小时前
doris用户资源组配置
数据库
Cloud云卷云舒7 小时前
云卷云舒:HaishanDB的AI原生能力主要应用场景
数据库·人工智能·ai-native·haishandb·移动云长期记忆
时间的拾荒人7 小时前
MySQL 视图详解
android·数据库·mysql
qq_366086228 小时前
sql 查询中关于 null 值判断的问题
数据库·sql
测试修炼手册8 小时前
[测试技术] JUnit 6 入门与实战:断言、参数化与 Mockito
数据库·junit·sqlserver