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跑出完全不同的结果集。有时候你以为只是换个数据库,结果换掉的是业务的逻辑。

相关推荐
明月_清风5 小时前
Muse 登顶 App Store 第一,SDK 直接开源:AI Agent 开始进入下一个阶段
人工智能·后端
沙漠之主7 小时前
C++编程教学设计资料:从入门到实战的完整课程方案
java·前端·c++
hasty7 小时前
不上传新包,也能改变用户拿到的版本:npm dist-tag 的 OIDC 权限治理
前端·npm·node.js
Csvn7 小时前
diff 算法(虚拟 DOM Reconciliation)
前端
hsfxuebao8 小时前
Loop Engineering 保姆级教程 + 项目实战
人工智能·后端
打工仔折腾 AI8 小时前
从 Demo 到生产级 Agent:8 个关键设计机制与 Python 实现拆解
java·jvm·人工智能·后端·python·langchain·ai agent 实战
架构技术专栏8 小时前
交叉熵:AI 怎样给概率预测打分
后端
前端snow8 小时前
ai agent --- 文件存储
前端
Frag0ut8 小时前
Chrome四大版本获取及共存指南:Stable/Beta/Dev/Canary
前端·chrome·浏览器·dev·beta·canary·共存版
IT_陈寒10 小时前
SpringBoot自动配置坑了我一把,原来是这样绕过去的
前端·人工智能·后端