国产化落地避坑 · 干货向|迁移后那条「测试能过、一上生产就查空」的 SQL,多半栽在 WHERE 函数顺序上 国产化迁移里最难查的从来不是那些一迁就报错的 SQL。报错至少给你一个明确的落脚点堆栈一摆问题在哪清清楚楚。真正折磨人的是那种测试环境跑得好好的、一上生产就时灵时不灵的 SQL。今天这条是我见过最典型的一个它甚至能在同一个会话里连跑几次结果还不一样。薛定谔的查询。你不点开结果集都不知道这次它到底返不返回。这类坑的根子往往是同一个误会把 SQL 当成过程式语言在用指望 WHERE 里的条件像代码一样一行一行按顺序执行。先看它长什么样。一、现象一条靠函数顺序控制流程的 SQL迁移或者重构的时候经常能翻出这种写法。开发想在一条 SQL 里同时干两件事一个函数负责设置状态另一个函数负责取状态而且取必须发生在设置之后。SELECT*FROMmy_tableWHEREid1pkg_abc.get_id()-- 先取值ANDpkg_abc.set_id(10)1;-- 再设值并返回 1写这条 SQL 的人心里预设的是 WHERE 里两个条件从左到右走set_id(10)先把包变量设成 10get_id()再把它取出来做过滤。听着挺顺。但放到真实环境里它的表现相当诡异。有时候能返回结果有时候静默返回空集什么都不报就是查不出东西。更离谱的是同一个会话里多跑几次结果还能不一致。一条 SELECT跑出了掷骰子的效果。二、两个内核到底怎么执行的要弄明白它为什么飘得看 Oracle 和 KES 各自怎么调度 WHERE 里的函数。在 Oracle 里WHERE 子句中两个对等的等值比较执行引擎通常按从左到右来。但这里藏着两个变数。一个是短路求值如果get_id()在左边先跑这时set_id()还没执行包变量是空的id1 get_id()直接判成 false短路之后右边的set_id()可能压根不会被触发于是整条 SQL 一行都返回不了。另一个更麻烦当等式和不等式混在一起时Oracle 的优化器可能优先调度某个函数条件执行顺序并不严格锁死在你写代码的位置上。KES 这边在兼容性设计上把顺序说得更明确。对 WHERE 子句里的函数条件不管是等式还是不等式系统默认按条件出现的先后从左到右依次执行。看到这你可能想那好办迁到 KES 顺序是确定的把 set 写前面、get 写后面不就行了。这恰恰是我要泼冷水的地方。KES 给了你一个确定的顺序不代表你就该依赖这个顺序。顺序确定只是让这条 SQL 在当前版本、当前写法下碰巧能跑对它的地基还是虚的。为什么虚接着往下看。三、为什么说依赖顺序本身就不安全第一包级全局变量会造成会话污染。无论 Oracle 还是 KES只要函数用到了 Package 级别的全局变量这个变量在整个会话存续期间都活着。这就埋了一个特别阴的假象。你在测试的时候大概率先跑过一条正确的 SQLset 在前变量被赋了值。之后哪怕你再跑那条错误的、get 在前的 SQL因为会话里还残留着上一次的值它照样能奇迹般地返回结果。你一看通过了放心上线。到了生产应用连的是连接池是一个个新开的会话。新会话里那个变量初始是空的没有哪一次历史执行帮它垫底程序当场失效。这种依赖历史状态的 SQL 是运维的噩梦因为它在你手里永远复现不出来只在生产的某个新连接上发作。第二优化器有权重写你的顺序。这一条更根本。SQL 是声明式语言不是过程式语言。你写的是我要什么不是按什么步骤做。至于用什么顺序、什么路径把结果算出来那是优化器的活。当前版本的执行器也许老老实实按左右顺序走但优化器的天职是找代价最低的路。哪天它评估下来觉得右边那个函数条件过滤率特别高或者计算代价特别低它完全有权把过滤顺序调过来。到那时候你精心设计的那条先 set 后 get 的逻辑链就被优化器一手打断了。而且它这么做是对的是它的本分。错的是你把业务流程的正确性压在了优化器的调度顺序上。一句话你依赖的这个顺序数据库从来没答应过要为你永远保证。四、避坑指南把有副作用的函数请出 WHERE 子句。这是最硬的一条。WHERE 是用来描述过滤条件的不是用来跑流程的。任何会修改数据或状态的函数也就是带副作用的函数都不该出现在 WHERE 里。正确的做法是把设置状态的逻辑挪到 SQL 外面先用程序或者存储过程调set_id把状态准备好再发起一条独立干净的 SELECT 去查。逻辑归逻辑查询归查询两件事拆开。纯读的函数老老实实声明属性。如果一个函数确实不改状态只做读取那就在 KES 里把它声明成IMMUTABLE或者STABLE。这不只是规范问题它能帮优化器真正理解这个函数的行为也能避免执行计划里出现没必要的重复调用。属性声明对了优化器才不用瞎猜。DBA 审计时盯住 Filter 的顺序。审慢 SQL 或者行为异常的 SQL 时重点看执行计划里 Filter 条件的顺序。用explain analyze把实际执行中每个条件的过滤顺序和耗时拉出来看。还有一个强信号如果你发现某条 SQL 的行为跟会话相关同一条语句换个会话结果就变那基本可以直接去查它是不是用了 Package 变量或者临时表。行为跟会话挂钩就是危险的味道。五、收尾回到那条掷骰子的 SELECT。它的问题从来不是顺序对不对而是它压根不该靠顺序活着。数据库执行引擎在特定配置下确实能给你一个稳定的函数执行顺序但 SQL 语义的安全不能建在巧合上面。用 WHERE 子句的先后来实现状态转换既违背了关系数据库声明式的设计初衷也给自己埋了一颗最难排查的雷。国产化迁移这条路之前聊过的外连接消除加上这一篇的函数顺序看着是两个不相干的点底下是同一件事。你以为 SQL 会照着你写的样子执行可数据库认的是语义不是你的书写顺序更不是你的主观意图。把这层缝对齐比让系统跑起来难得多也重要得多。逻辑归逻辑查询归查询。这句话值得贴在每一个迁移项目组的墙上。清单继续下一个坑接着写。