LEFT JOIN 凭空消失的背后,是优化器偷偷帮你做了决定

LEFT JOIN 凭空消失的背后,是优化器偷偷帮你做了决定

一个让我懵了的执行计划

上个月帮同事排查一个慢查询,SQL写得挺规矩:

sql 复制代码
SELECT * FROM orders o LEFT JOIN refunds r ON o.id = r.order_id WHERE r.status = 1;

业务方的意思是:查所有订单,有退款的把退款信息带出来,没有的就算了。

同事说这个查询跑得慢,让我看看执行计划有没有问题。我拉出来一看,愣住了------执行计划里压根没有LEFT JOIN,就是个普通的Hash Join。

我说你这SQL不对吧?他信誓旦旦说就是LEFT JOIN。我重新跑了一遍计划,确实没有外连接。

当时我就纳闷了,LEFT JOIN哪去了?

查了半天才发现,金仓的优化器把LEFT JOIN自动转成了INNER JOIN。因为WHERE条件里有r.status = 1,优化器算了一笔账:右表没匹配上的行,r.status是NULL,NULL = 1是false,早晚会被WHERE过滤掉,那我干脆先转成INNER JOIN,效率更高。

逻辑上没毛病,但业务语义变了。

那些没退款的订单,因为右表没有匹配行,本该保留下来。结果被优化器这么一搞,全没了。这个SQL其实应该写成:

sql 复制代码
SELECT * FROM orders o LEFT JOIN refunds r ON o.id = r.order_id AND r.status = 1;

把条件放ON里,先过滤退款表再连接,订单才能全保留。

优化器在想什么

数据库的查询优化器,说穿了就是一门算账的生意。它拿到一条SQL,会生成无数种执行方式,然后挑一个它觉得最便宜的。

外连接消除这件事,本质上就是优化器在算一笔账:外连接(LEFT JOIN)比内连接(INNER JOIN)贵在哪?

内连接的执行逻辑是:两个表匹配上了就返回,匹配不上就扔掉。外连接不一样,左表的行不管右表有没有匹配都得留着,这天然就比内连接多干活。

所以当优化器发现,WHERE条件会把那些"右表没匹配上"的行全部干掉,它就觉得没必要做外连接了。反正结果跟内连接一样,干嘛不用更快的?

这就是优化器的逻辑:它不管你写的是什么SQL,它只关心最终结果集是不是一样。如果一样,它就选最便宜的。

什么样的条件会触发消除

简单说,只要WHERE条件里包含了右表的非空列,并且这个条件在NULL上返回false,优化器就可能动手。

我列几个常见的:

sql 复制代码
-- 等值条件
WHERE t2.status = 1

-- 范围条件  
WHERE t2.amount > 100

-- 模糊匹配
WHERE t2.name LIKE '张%'

-- IN条件
WHERE t2.id IN (1,2,3)

这些条件在NULL上都是false,所以都会触发消除。

那什么时候不会消除?

sql 复制代码
-- IS NULL 不会触发
WHERE t2.id IS NULL

-- 条件在左表不会触发
WHERE t1.status = 1

查"右表没匹配"本来就是LEFT JOIN的核心用途,优化器不敢动这个。条件放左表相当于先筛选用户,再外连接,也不影响。

金仓在这里做了什么

金仓的优化器在处理外连接消除时,内部走了一个叫"外连接消除检查"的流程。它会扫描WHERE条件里所有引用右表的表达式,然后判断这些条件在NULL上的表现。

具体怎么判断的?

对于每个条件,优化器会问三个问题:

  1. 这个条件在NULL上返回true还是false?
  2. 如果返回false,这个条件会不会引用右表的其他列?
  3. 有没有其他条件能保住这些NULL行?

如果所有条件在NULL上都返回false,并且没有IS NULL这类保底条件,优化器就判定外连接可以消除。

注意这里还有个细节:如果右表的列允许NULL,优化器会更谨慎。比如t2.name如果是可空的,那t2.name = 'cc'这个条件在NULL上是false,但在真实数据中如果有NULL值,WHERE t2.name = 'cc'本来就会过滤掉NULL,所以消除是安全的。

但如果是t2.name IS NOT NULL这种条件,它在NULL上是false,但语义上要求排除NULL值,外连接产生的NULL行确实应该被过滤,所以消除也是安全的。

总的来说,只要条件能准确排除所有NULL行,优化器就敢动。

我见过的一些翻车现场

翻车的原因其实就一个:开发者不知道自己写的WHERE条件会被优化器"解读"成什么。

翻车现场一:迁移过来的老代码

这是最常见的。原来在MySQL上跑得好好的SQL,迁移到金仓上数据就少了。因为MySQL的优化器在早期版本里不会做这个转换。

我见过一个项目,迁移完上线第一天,报表数据就对不上。排查发现全是LEFT JOIN写法和WHERE条件混用。改了几十条SQL才稳住。

翻车现场二:复杂的嵌套视图

视图里面套视图,外层再套WHERE条件。这时候优化器在做视图折叠和外连接消除的时候,条件传递路径复杂,容易产生非预期的消除。

比如A视图里有个LEFT JOIN,外层查询对右表加了条件,优化器可能在外层就把外连接消了,而开发者根本不知道视图里原来写的是外连接。

翻车现场三:OR条件下误判

如果WHERE条件是t2.col = 1 OR t2.col IS NULL,这种情况优化器不会消除,因为IS NULL保住了NULL行。但复杂的OR条件,特别是涉及多个表的,优化器可能分析不到那么深。

怎么应对

说白了就一句话:外连接的右表条件,除非确定要过滤空行,否则放ON里

如果目标是"查所有左表行,右表只匹配满足条件的行",条件放ON:

sql 复制代码
LEFT JOIN t2 ON t1.id = t2.id AND t2.status = 1

如果目标是"只查右表有匹配且满足条件的行",干脆用INNER JOIN:

sql 复制代码
INNER JOIN t2 ON t1.id = t2.id AND t2.status = 1

或者把条件放WHERE也行,但那就别写LEFT JOIN了,容易让人误解。

还有一点,金仓支持Oracle的(+)语法,但用的时候要小心:WHERE里的条件如果不带(+),照样会触发消除。如果条件也带了(+),才等同于放ON里。

金仓优化器的设计取舍

其实金仓可以做两件事来避免这种问题:一是默认不做外连接消除,让用户自己控制;二是加一个开关让用户关掉这个优化。

但这两条路都有问题。不做消除,很多SQL的性能会受损。加开关,99%的用户不会去改默认值。

所以金仓的选择是:默认开启,让优化器帮你省钱,但你得知道这个功能的存在,写SQL的时候注意规范。

这也引出一个更大的话题:数据库优化器到底应该多聪明?

太笨了,性能上不去。太聪明了,用户又不知道它在背后干了什么。

我个人倾向于优化器可以聪明,但得把决策过程告诉用户。比如执行计划里如果能显式标注"OUTER JOIN ELIMINATED",至少出了问题我能知道是优化器干的。

不过目前金仓的执行计划里看不出这个标记,所以只能靠经验和执行计划的形状来判断。

说到底

外连接消除本身不是坏事。它让SQL跑得更快,逻辑上也没错。问题在于它太隐蔽了,开发者很难感知到这条SQL已经悄悄变了味。

跨数据库迁移的时候尤其要小心,不同优化器的行为差异,可能让你的SQL跑出完全不同的结果集。有时候你以为只是换个数据库,结果换掉的是业务的逻辑。

相关推荐
HackTwoHub1 小时前
AntiDebug Mcp、AI逆向绕过前端,Hook 加密接口,抓取 Vue 路由漏洞,接入 MCP 让 AI 自动化挖掘前端漏洞
前端·vue.js·人工智能·安全·web安全·网络安全·系统安全
Mr_凌宇1 小时前
AI 应用工程进阶扩展学习笔记 二
前端·面试
阿维的博客日记1 小时前
spring里面的@RequestParam什么意思
java·后端·spring
金金金__1 小时前
腾讯云COS接入配置
后端
Hyyy1 小时前
Git Worktree 完全讲解
前端·后端·面试
葡萄城技术团队2 小时前
AI 不会淘汰电子表格,反而会让电子表格成为 AI 操作系统
前端·人工智能
BigTopOne2 小时前
Short Put and Short Call
前端
逝水无殇2 小时前
C# 反射详解
开发语言·后端·c#
Cache技术分享2 小时前
465. Java 反射 - 获取类的简单名
前端·后端