国产化落地避坑 · 干货向|迁移后那条「测试能过、一上生产就查空」的 SQL,多半栽在 WHERE 函数顺序上

国产化迁移里,最难查的从来不是那些一迁就报错的 SQL。报错至少给你一个明确的落脚点,堆栈一摆,问题在哪清清楚楚。真正折磨人的,是那种测试环境跑得好好的、一上生产就时灵时不灵的 SQL。今天这条,是我见过最典型的一个,它甚至能在同一个会话里连跑几次,结果还不一样。

薛定谔的查询。你不点开结果集,都不知道这次它到底返不返回。

这类坑的根子,往往是同一个误会,把 SQL 当成过程式语言在用,指望 WHERE 里的条件像代码一样一行一行按顺序执行。

先看它长什么样。

一、现象,一条靠函数顺序控制流程的 SQL

迁移或者重构的时候,经常能翻出这种写法。开发想在一条 SQL 里同时干两件事,一个函数负责设置状态,另一个函数负责取状态,而且取必须发生在设置之后。

sql 复制代码
SELECT * FROM my_table
WHERE id1 = pkg_abc.get_id()   -- 先取值
  AND pkg_abc.set_id(10) = 1;  -- 再设值,并返回 1

写这条 SQL 的人,心里预设的是 WHERE 里两个条件从左到右走,set_id(10) 先把包变量设成 10,get_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 级别的全局变量,这个变量在整个会话存续期间都活着。这就埋了一个特别阴的假象。

你在测试的时候,大概率先跑过一条正确的 SQL,set 在前,变量被赋了值。之后哪怕你再跑那条错误的、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 会照着你写的样子执行,可数据库认的是语义,不是你的书写顺序,更不是你的主观意图。把这层缝对齐,比让系统跑起来难得多,也重要得多。

逻辑归逻辑,查询归查询。这句话,值得贴在每一个迁移项目组的墙上。

清单继续,下一个坑接着写。

相关推荐
蓝田~6 小时前
MySQL慢查询怎么优化?B+Tree索引原理+MVCC读不阻塞写+EXPLAIN执行计划,从5秒到0.05秒
数据库·mysql
sunxunyong6 小时前
doris用户资源组配置
数据库
Cloud云卷云舒7 小时前
云卷云舒:HaishanDB的AI原生能力主要应用场景
数据库·人工智能·ai-native·haishandb·移动云长期记忆
时间的拾荒人7 小时前
MySQL 视图详解
android·数据库·mysql
qq_366086227 小时前
sql 查询中关于 null 值判断的问题
数据库·sql
测试修炼手册7 小时前
[测试技术] JUnit 6 入门与实战:断言、参数化与 Mockito
数据库·junit·sqlserver
吴声子夜歌7 小时前
Redis 3.x——集群故障转移
java·数据库·redis·集群
名不经传的养虾人8 小时前
从0到1:企业级AI项目迭代日记 Vol.71|系统显示正常,但实际上什么都没发生
数据库·人工智能·ai编程·ai工作流·企业ai
尽兴-8 小时前
企业数据库选型与演进:Oracle、达梦、MySQL 与分库分表实践
数据库·mysql·oracle·分库分表