踩坑实战向|MySQL/PostgreSQL 迁移必踩:LEFT JOIN 莫名丢数据,金仓 KES 优化行为避坑指南
最近帮团队做异构数据库迁移,从 MySQL/PostgreSQL 迁到金仓 KES。业务逻辑没改,SQL 也没动,上线后却发现报表数据莫名其妙变少了。排查一圈下来,问题居然出在一个“看起来很合理”的 LEFT JOIN 写法上。本文把这次踩坑过程、原理分析和解决方案梳理出来,希望能帮大家少走弯路。
一、问题现场:LEFT JOIN 怎么就把数据“吞”了?
先还原一下当时的业务场景。
我们有两张表:
-
t1:订单主表(左表,驱动表) -
t2:订单扩展信息表(右表)
业务需求很简单:查出所有订单,如果扩展信息表中存在 name2 = 'cc'的记录,就一并展示;如果没有,扩展字段显示 NULL。
当时开发同学写的 SQL 如下(为了方便阅读,做了简化):
SELECT *
FROM t1
LEFT JOIN t2 ON t1.id1 = t2.id2
WHERE t2.name2 = 'cc';
在原来的 MySQL / PostgreSQL 环境中,这条 SQL 的行为是“符合直觉”的:
-
t1的所有订单都会返回; -
如果
t2中有匹配且name2 = 'cc',则显示对应字段; -
否则,
t2相关字段为 NULL。
然而,迁移到 金仓 KES 后,诡异的事情发生了:
结果集中,大量原本应该出现的
t1记录消失了。
第一反应是:是不是数据没同步全?是不是有触发器没迁过来?查了一圈,数据没问题。
于是把执行计划拉出来一看,真相浮出水面:
-> Hash Join (cost=...)
Hash Cond: (t1.id1 = t2.id2)
-> Seq Scan on t1
-> Hash
-> Seq Scan on t2
Filter: (name2 = 'cc')
注意到了吗?计划中并没有出现 Left Join,而是直接变成了 Hash Join(内连接)。
也就是说,KES 的优化器在这里做了一个等价变换:外连接消除(Outer Join Elimination)。
二、为什么会这样?Nullable-Side 条件的“隐式强约束”
要理解这个问题,得先回到 SQL 的执行语义上。
1. SQL 的逻辑执行顺序
在标准 SQL 中,一条带 JOIN 的查询,逻辑上的执行顺序是:
-
FROM / JOIN:先做连接,生成中间结果集
-
WHERE:对中间结果集进行过滤
-
SELECT:投影需要的列
-
ORDER BY / LIMIT 等
对于 LEFT JOIN来说:
-
如果右表(
t2)没有匹配行,那么t2的所有列都会被填充为NULL; -
这些“补出来的 NULL 行”,依然属于
LEFT JOIN的结果集。
2. Nullable-Side 条件带来的“副作用”
现在看我们的 SQL:
WHERE t2.name2 = 'cc';
这里的 t2.name2属于 Nullable-Side(右表的非主键列)。
考虑两种情况:
-
情况 A:
t2有匹配行,且name2 = 'cc'→ 条件成立,保留; -
情况 B:
t2没有匹配行 →t2.name2为NULL→NULL = 'cc'的结果是Unknown→ 该记录被 WHERE 过滤掉。
也就是说:
所有由 LEFT JOIN 产生的 NULL 行,都会被这个 WHERE 条件无情地干掉。
3. 优化器的“等价变换”逻辑
站在优化器的角度看问题:
-
既然
WHERE t2.name2 = 'cc'会把所有右表为 NULL 的行全部过滤掉; -
那么,“LEFT JOIN + WHERE 过滤”的最终结果,在数学逻辑上完全等价于“INNER JOIN + WHERE 过滤”。
为了降低执行代价,优化器就会选择:
把 LEFT JOIN 改写成 INNER JOIN(外连接消除)
这是一个完全合法、符合 SQL 标准的优化行为,但在业务语义上,却往往不是开发人员想要的。
三、什么时候不会消除?IS NULL 是个例外
并不是所有涉及右表的条件都会触发外连接消除。
一个非常典型的反例是 IS NULL。
SELECT *
FROM t1
LEFT JOIN t2 ON t1.id1 = t2.id2
WHERE t2.name2 IS NULL;
这条 SQL 的业务语义是:找出在 t2 中没有匹配记录的 t1 行。
如果优化器把它转成内连接,那这些“缺失匹配”的记录根本不可能出现在结果集中,逻辑就完全错了。
因此,在 KES 中,这类包含 Nullable-Side IS NULL的查询:
-
不会触发外连接消除;
-
执行计划中依然会保留
Left Join的特征。
这也是为什么很多“找差集”的 SQL(比如“查找未绑定扩展信息的订单”)在迁移过程中反而表现稳定的原因。
四、Oracle (+)语法下的坑(KES 兼容模式)
顺带一提,我们在迁移过程中还遇到了 Oracle 风格外连接的写法。
KES 支持 Oracle 的 (+)语法,但坑也不少。
1. 错误示例
SELECT *
FROM t1, t2
WHERE t1.id1 = t2.id2(+)
AND t2.name2 = 'cc';
这里 (+)只出现在 JOIN 条件上,而 t2.name2 = 'cc'是普通 WHERE 条件。
效果和前面 ANSI JOIN 的例子一模一样:外连接被消除,数据“丢失”。
2. 正确写法
如果确实想保留左表全部数据,同时过滤右表,需要把过滤条件也标记为外连接侧:
SELECT *
FROM t1, t2
WHERE t1.id1 = t2.id2(+)
AND t2.name2(+) = 'cc';
这种写法在语义上等同于:
SELECT *
FROM t1
LEFT JOIN t2 ON t1.id1 = t2.id2 AND t2.name2 = 'cc';
不过,从可读性和可维护性角度,我更推荐直接使用 ANSI JOIN 语法,避免 (+)这种“上古语法”带来的心智负担。
五、Non-Nullable-Side 条件:左表条件的影响
还有一种容易混淆的情况:条件写在左表上。
SELECT *
FROM t1
LEFT JOIN t2 ON t1.id1 = t2.id2
WHERE t1.name1 = 'a';
这里 t1.name1属于 NonNullable-Side。
这个条件的语义是:
-
先筛选出
t1.name1 = 'a'的记录; -
再对这些记录做 LEFT JOIN。
结果是:不符合 t1.name1 = 'a'的左表记录被过滤掉,这是符合预期的。
但如果把条件写到 ON子句中:
SELECT *
FROM t1
LEFT JOIN t2 ON t1.id1 = t2.id2 AND t1.name1 = 'a';
语义就变成了:
-
t1所有记录都参与外连接; -
只有
t1.name1 = 'a'的行才会尝试匹配t2; -
其他
t1行仍然会出现在结果集中,只是t2字段为 NULL。
一句话总结:
-
WHERE是对最终结果集的过滤; -
ON是对连接行为的控制。
这个原则在 KES 中同样适用,也是排查外连接行为异常的重要切入点。
六、迁移实战中的排查套路
在这次迁移项目中,我们总结出了一套比较实用的排查流程,供大家参考。
1. 先看执行计划
在 KES 中,可以用:
EXPLAIN (ANALYZE, VERBOSE)
SELECT ...
重点关注:
-
JOIN 类型是否还是
Left Join; -
有没有变成
Hash Join/Nested Loop但没有标明 Left 语义; -
过滤条件被下推到了哪一步。
如果发现原本期望的外连接在计划中“消失”,就要警惕外连接消除了。
2. 明确业务语义
拿到一条 SQL,先问自己三个问题:
-
是否必须保留左表全部记录?
-
过滤条件到底是限制左表,还是限制右表?
-
是否允许右表为 NULL?
如果答案是“必须保留左表 + 过滤右表”,那么:
-
右表的过滤条件一定要放到
ON子句中。
3. 改写 SQL 的正确姿势
回到最开始的例子,正确的写法应该是:
SELECT *
FROM t1
LEFT JOIN t2 ON t1.id1 = t2.id2
AND t2.name2 = 'cc';
或者,如果逻辑上更清晰,也可以写成子查询形式(视执行计划而定):
SELECT *
FROM t1
LEFT JOIN (
SELECT * FROM t2 WHERE name2 = 'cc'
) t2_filtered
ON t1.id1 = t2_filtered.id2;
在实际测试中,这两种写法在 KES 中都能稳定保持外连接语义,不会被优化器“偷偷”转成内连接。
七、性能与一致性的取舍
有同学可能会问:
外连接消除不是一种性能优化吗?为什么要抵制它?
这是一个非常好的问题。
在纯性能视角下,外连接消除确实是好事:
-
内连接可以选择更丰富的连接算法;
-
可以减少中间结果集大小;
-
在很多 OLTP 场景下能显著降低延迟。
但在 数据库迁移场景 中,优先级是不一样的:
-
第一优先级:逻辑一致性
-
报表数字不能错;
-
业务行为不能变;
-
迁移前后结果必须一致。
-
-
第二优先级:性能
-
在逻辑正确的前提下再做调优;
-
必要时可以通过 hint 或 SQL 改写控制执行计划。
-
因此,在 KES 迁移项目中,我的建议是:
-
默认假设优化器会做外连接消除;
-
显式写出你想要的语义(条件放 ON);
-
再通过执行计划验证,而不是“赌优化器不动它”。
八、总结与避坑清单
这次踩坑,本质上是一次“优化器聪明过头”的典型案例。
为了避免大家重蹈覆辙,我把关键点整理成一个简短的避坑清单:
-
LEFT JOIN + 右表 WHERE 条件 = 高风险
-
除非你明确只想查匹配成功的记录;
-
否则请把右表过滤条件挪到
ON子句。
-
-
执行计划是第一真相来源
-
不要只看 SQL 文本;
-
要看计划中 JOIN 的类型是否发生变化。
-
-
ON vs WHERE,语义截然不同
-
ON:控制连接行为; -
WHERE:控制最终结果。
-
-
Oracle
(+)语法要慎用-
过滤条件是否带
(+),结果可能完全不同; -
迁移时优先考虑改成 ANSI JOIN。
-
-
迁移阶段以一致性为先
-
性能可以慢慢调;
-
数据错误是事故。
-
写在最后
数据库迁移从来不是“语法替换”这么简单,优化器行为的差异往往是最大的隐形雷区。
这次在金仓 KES 上遇到的外连接消除问题,其实在其他数据库中也有类似机制,只是在 KES 的优化策略下表现得更加“积极”。理解其背后的 Nullable-Side 逻辑,不仅能帮你避开迁移坑,也能让你在日常 SQL 编写中写出更健壮、语义更清晰的查询。
如果你也在做 MySQL / PostgreSQL 到 KES 的迁移,欢迎在评论区聊聊你遇到的奇葩问题,我们一起填坑。
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐




所有评论(0)