金仓KES优化器:外连接消除的奥秘与实战 做技术久了就会发现数据库优化器有时候像个调皮但又极度聪明的孩子。它能把你的 SQL 优化得飞起但如果你没摸清它的脾气它也能把你的业务逻辑改得面目全非。今天来聊一个“外连接消除”就是这样一个典型的场景。很多从 Oracle 或者其他传统数据库迁移到金仓 KES 的团队经常会遇到开发同学气急败坏地跑过来“我的数据丢了金仓有 Bug” 排查半天最后发现根本不是 Bug而是优化器觉得“这段代码没必要按你写的那么执行”于是自作主张给优化了。这到底是怎么一回事一、 现象LEFT JOIN 怎么就“缩水”成了 INNER JOIN我们先来还原一个最常见的翻车现场。假设我们在金仓 KES 里有一张核心业务表 t1比如订单表和一张扩展属性表 t2比如订单的物流追踪表。业务需求很简单我要查出所有的订单顺带看看有没有对应的物流信息。如果有展示出来如果没有订单本身也不能丢。这是一个非常标准的左外连接场景。开发同学大笔一挥写出了如下 SQLSELECT t1.id, t1.name, t2.track_code FROM t1 LEFT JOIN t2 ON t1.id t2.order_id WHERE t2.track_code SUCCESS;乍一看逻辑天衣无缝。但在金仓 KES 的实际执行中事情却变得诡异起来。当你满怀期待地敲下回车或者在测试环境跑通了逻辑信心满满地把这套代码推送到基于 KES 的生产环境时却惊讶地发现咦怎么 t1 里那几条没有物流状态即 t2 中没有匹配记录的订单不见了执行计划里原本明明白白写着 Left Join 的算子居然变成了一个普通的 Hash Join。这时候有些同学开始怀疑人生了是不是金仓 KES 对 LEFT JOIN 的支持有问题是不是驱动表选错了别慌这不是问题这是优化器在“炫技”。二、 扒开底裤优化器的“小心思”与关系代数要理解优化器干了什么我们得稍微把视角往底层挪一点看看数据库内核到底是怎么看待这段 SQL 的。在 SQL 的标准语义里LEFT JOIN 的核心使命是什么是保左。也就是说无论右边的表能不能匹配上左边表的每一行数据都必须出现在最终结果集中。如果右边匹配不上那右边的列就填 NULL。但是请注意上面那段 SQL 的 WHERE 子句t2.track_code SUCCESS。这就出现了一个逻辑冲突。LEFT JOIN 为了让左表全量输出会在右表找不到数据时强行塞一个 NULL 进去。而紧接着的 WHERE 条件却在找 t2.track_code 等于某个具体值的行。在关系代数里NULL SUCCESS的结果是什么是 Unknown未知而在 SQL 的三值逻辑中Unknown 会被判定为假。这就意味着只要经过了 LEFT JOIN那些右表没匹配上的行立刻就会被后面的 WHERE 条件给“杀”掉。优化器在生成执行计划前会进行一遍“逻辑等价检查”Logical Equivalence。它一看哎你这个 LEFT JOIN 后面接了个针对右表的等值过滤直接把 LEFT JOIN 产生的 NULL 行全过滤没了。那这个操作在数学逻辑上和一个普通的 INNER JOIN 然后过滤结果是一模一样的啊既然结果一样那我干嘛还要费劲巴拉地去做外连接直接转成内连接吧内连接的算法成熟多了Hash Join 一把梭速度飞快。这就是外连接消除的本质。它不是 Bug而是基于关系代数的一种合法等价变换。优化器认为既然你最后都要把右表没数据的行干掉那我还不如一开始就不保留它们还能省点内存和 CPU。三、 金仓 KES 优化器的设计思路拆解知道了原理我们再来看看金仓 KES 在这块是怎么落地的。作为一款面向企业级核心业务的国产数据库KES 的优化器设计思路是非常稳健的。它不是那种为了极致性能可以牺牲一切野路子而是在正确性Correctness和性能Performance之间找平衡。1.触发条件Nullable-Side 的“致命诱惑”在 KES 的代码逻辑里外连接消除的触发条件非常明确WHERE 子句中必须存在针对“可空侧”Nullable-Side也就是 LEFT JOIN 的右边表或者 RIGHT JOIN 的左边表的非空过滤条件。这个条件通常长这样• table.column constant 如前面的 t2.track_code SUCCESS• table.column IS NOT NULL一旦优化器在语法树里嗅到了这种味道它就会把“外连接转内连接”这个优化选项标记为潜在可用。2.底线防守IS NULL 的保命符但是优化器也不是无脑的。它很聪明知道什么时候该停手。如果 WHERE 子句里写的不是 SUCCESS而是t2.track_code IS NULL呢这时候优化器绝对不会触发消除。为什么因为 IS NULL 的语义变了。它的目的不再是“过滤掉没用的行”而是专门为了“抓取那些在右表没有匹配上的行”。这正是 LEFT JOIN 存在的意义之一用来找差集。如果这时候优化器还自作聪明把它转成内连接那业务逻辑就彻底崩了——内连接是绝对不可能查出 NULL 的。所以在 KES 中只要你在 WHERE 里对外连接表的字段用了 IS NULL优化器就会乖乖锁死外连接属性绝不再动歪脑筋。3.代价估算什么时候才真正动手仅仅满足上面的逻辑等价还不够。优化器是很现实的。它还要算一笔账我把它转成内连接到底能省多少资源KES 内部维护了一套复杂的统计信息系统PG_STATISTIC。它会根据表的大小、索引情况、字段的离散度去估算转换前后的 Cost。如果右表极小或者内连接能完美利用上 Hash JoinCost 大幅下降那优化器就会果断下手。但如果右表极大转换后并没有明显的 Cost 优势或者由于统计信息不准导致估算失误优化器可能会选择保守策略保留原本的外连接。四、 工程实践如何驯服优化器搞懂了原理最终还是要落脚到代码上。在日常的 Java 后端开发或者 DBA 调优中我们应该怎么写才能既让优化器发挥实力又不破坏我们的业务逻辑1.SQL 重构把条件放回 ON 子句这是最正经、最标准的做法。如果你的需求确实是“查所有订单但只关联状态为 SUCCESS 的物流信息没有 SUCCESS 的物流信息也不要”那么这个过滤条件其实属于“连接规则”的一部分而不是“最终结果筛选”。你应该这样写SELECT t1.id, t1.name, t2.track_code FROM t1 LEFT JOIN t2 ON t1.id t2.order_id AND t2.track_code SUCCESS;看出区别了吗把t2.track_code SUCCESS从 WHERE 挪到 ON 后面。在 KES 的执行逻辑里ON子句是在JOIN发生的那一瞬间起作用的。这时候优化器一看哦你这是在连接的时候就限制了右表的数据源这属于正常的 LEFT JOIN 范畴。它就不敢乱动外连接属性得以保留。2.警惕 Oracle 方言的“暗礁”很多团队在做国产化替代时是从 Oracle 迁移过来的。Oracle 有一种古老的外连接语法叫 ()。在金仓 KES 中为了兼容生态也是支持这种语法的。但这里面坑特别多。比如这种写法WHERE t2.track_code SUCCESS AND t1.id t2.order_id()KES 内核在解析时会把这个()翻译成标准的 LEFT JOIN。但因为这个等值条件写在了WHERE里优化器依然会觉得这是一个针对右表的过滤从而触发外连接消除。如果你不想被消除必须把带()的条件也放到 JOIN 的条件里去。所以强烈建议在迁移项目中弃用()语法全面拥抱 ANSI 标准的JOIN/ON语法。这样不仅 SQL 可读性更好也能避免优化器因为解析歧义而产生非预期的优化。3.脏数据的“照妖镜”有时候外连接消除也能帮我们发现业务代码里的逻辑漏洞。假设你的 SQL 里写了 LEFT JOIN但 WHERE 条件里却莫名其妙地对右表的一个字段做了非空判断比如前端传了个参数后端直接拼到了 WHERE 里。如果有一天这个查询突然变快了但数据少了你大概率就能想到是优化器把 LEFT JOIN 给“优化”没了。这其实是一件好事。它像一个照妖镜逼着开发者把业务逻辑梳理得更清晰你到底是想保留左表全量还是只想查关联上的数据五、 结语数据库优化器是一门平衡的艺术。金仓 KES 的外连接消除机制正是这种艺术的体现。它利用关系代数的数学之美在不改变结果集的前提下榨取硬件的最后一滴性能。作为技术人员我们不需要去读懂 KES 内核里那几千行的 C 代码但我们必须建立起一种“优化器思维”。当我们写 SQL 时要知道不仅是写给数据库去“执行”更是写给优化器去“沟通”。明确你的过滤条件是属于“连接过程”还是“结果筛选”合理运用 ON 和 WHERE避开那些隐式转换的坑。只有这样我们才能和金仓 KES 这样的优秀国产数据库配合默契写出既高效又稳健的业务代码。好了今天的碎碎念就到这里。你在金仓 KES 的使用中还遇到过哪些优化器给你带来的“惊喜”或“惊吓”欢迎在评论区留言交流附录完整可复现代码脚本为了方便大家在自己环境的金仓 KES 中复现我把文中提到的所有 SQL 整理成了一个完整的脚本-- 1. 建表 CREATE TABLE t1 ( id SERIAL PRIMARY KEY, name VARCHAR(50) ); CREATE TABLE t2 ( id SERIAL PRIMARY KEY, order_id INT, track_code VARCHAR(50) ); -- 2. 插入测试数据 INSERT INTO t1 (name) VALUES (订单1), (订单2), (订单3); INSERT INTO t2 (order_id, track_code) VALUES (1, SUCCESS), (1, FAILED), (2, SUCCESS); -- 3. 场景A会触发外连接消除的写法数据会“丢失” SELECT t1.id, t1.name, t2.track_code FROM t1 LEFT JOIN t2 ON t1.id t2.order_id WHERE t2.track_code SUCCESS; -- 4. 场景B不会触发消除的写法保留左表全量 SELECT t1.id, t1.name, t2.track_code FROM t1 LEFT JOIN t2 ON t1.id t2.order_id AND t2.track_code SUCCESS; -- 5. 场景C利用 IS NULL 查找缺失数据绝对不消除 SELECT t1.id, t1.name FROM t1 LEFT JOIN t2 ON t1.id t2.order_id WHERE t2.track_code IS NULL;