上线前能跑,上线后挂了,三个隐性的SQL陷阱

上线前能跑,上线后挂了,三个隐性的SQL陷阱

上线的第二天,功能突然不工作了

前年做了一个国产化迁移项目,从Oracle换到金仓。系统上线之后,头两天风平浪静。第三天开始,有个模块偶尔报错,再过一天直接崩溃了。最开始只是偶尔返回空结果,后来直接不干活了。

业务方很着急:"你们换数据库是不是把逻辑搞乱了?"

我查了半天代码,最后发现是WHERE子句里函数调用顺序的问题。

Oracle里的写法是这样的:

sql 复制代码
SELECT * FROM my_table 
WHERE id1 = pkg_abc.get_id() 
AND pkg_abc.set_id(10) = 1;

这个逻辑很明显:先给变量赋个值,再拿这个值去查数据。

在Oracle里,条件是从左往右执行的。get_id()先跑,变量没值,查不到东西。但因为set_id(10)=1会在get_id()之前被先执行(Oracle里赋值函数被当作常量条件优先处理了),所以实际能跑通。

到了金仓上,执行顺序变了。金仓在兼容性设计上虽然也按从左到右处理,但这种"赋值函数"和"取值函数"混在一起的情况,执行结果极其不稳定。刚开始还能返回几条,后来干脆全返回空。

问题的根源在于:优化器会根据代价判断执行顺序。有时候它觉得get_id()更便宜,就先跑get_id(),变量还没赋上,结果自然是空。有时候它觉得set_id()更便宜,先跑了,数据又能出来。

结果就是同一个SQL,有时候行有时候不行。

更坑的是,如果当前会话里之前执行过正确的SQL,变量已经被赋过值了,后面再跑错误的SQL也能拿到数据。这会造成"测试通过了"的假象,上线之后才发现问题。

后来我重写了一下,把这两个函数调用分开了:

sql 复制代码
-- 第一步:设置值
SELECT pkg_abc.set_id(10) FROM DUAL;

-- 第二步:查询数据
SELECT * FROM my_table WHERE id1 = pkg_abc.get_id();

两步走,逻辑清晰,不管在哪个数据库上跑都一样。

这件事之后我学乖了:WHERE子句里千万别放有副作用的函数。什么叫副作用?就是会改数据、改状态、改变量值的函数。这类函数应该单独执行,不要跟查询混在一起。

迁移后少了一条数据

另一个项目,从MySQL迁到金仓。

迁移完第二天,业务方反馈某个报表少了一条数据。查了半天,发现是LEFT JOIN的问题:

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

这条SQL的意思是:查所有订单,有退款的带上退款信息,没退款的就算了。

MySQL里跑出来1280条,金仓里只有1123条,少了157个没退款的订单。

原因我后来才搞清楚:金仓的优化器会把LEFT JOIN转成INNER JOIN。WHERE条件里有r.status = 1,右表没匹配的行r.status是NULL,NULL不等于1,这些行会被干掉。优化器一算:既然这些行最后都要被过滤掉,我直接转成INNER JOIN算了。

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

正确的写法应该是把条件挪到ON里:

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

这样优化器就没理由消除了,因为连接的时候右表已经过滤过了,左表的所有行都能保住。

后来我养成了个习惯:凡是外连接,右表的过滤条件一律放ON里。除非你确实想把没匹配的行过滤掉,那直接用INNER JOIN就行了。

类型不匹配带来的性能爆炸

还有一次,迁移完发现某个查询慢得离谱。原系统里毫秒级响应,到了金仓上变成几十秒。

查了一圈,发现是数据类型的问题。

原SQL里有个条件:

sql 复制代码
SELECT * FROM orders WHERE order_no = '123456';

order_no在表里是VARCHAR类型,传参传的是字符串,没问题。

但另一个地方是这么写的:

sql 复制代码
SELECT * FROM orders WHERE order_no = 123456;

传的是数字。MySQL里能做隐式类型转换,VARCHAR跟数字比的时候,会把字符串转成数字再比较。索引还能用上。

金仓的处理方式不一样。数字和VARCHAR比较的时候,会把VARCHAR转成数字。但问题是,这个转换是逐行执行的,索引用不上了,只能全表扫描。100万条数据,全部扫一遍,几十秒就出去了。

查了一下执行计划,确实是全表扫描。

改起来很简单,统一用字符串就行:

sql 复制代码
SELECT * FROM orders WHERE order_no = '123456';

但问题在于,代码里这种地方太多了。有些是前端传过来的参数类型不一致,有些是ORM自动生成的SQL,有些是不同模块的开发写了不同的写法。

最后我把所有传参的地方都统一了,确保传的是字符串。然后加了一条规则:代码里传参必须跟字段类型一致,不能依赖隐式转换。

总结一下

这三个陷阱有个共同点:在原来的数据库上能跑,到了新数据库上就不行了。

第一个是函数调用顺序的问题。Oracle里能依赖执行顺序的写法,到了金仓上就不稳定了。解决办法是把有副作用的函数移出WHERE子句。

第二个是优化器行为差异的问题。MySQL不做的优化,金仓做了。外连接的右表条件,放到ON里就是安全的。

第三个是隐式类型转换的问题。MySQL里能用的隐式转换,金仓里不一定能用,索引可能失效。保持类型一致,不要图省事。

后来我总结出几条迁移排查的经验,不一定全面,但至少能帮你少走些弯路:

先看执行计划。 慢查询、结果对不上,第一件事就是拉执行计划。一看计划就知道优化器干了什么。

再看函数调用。 WHERE子句里有自定义函数吗?函数里有状态变更吗?有的话先改掉。

最后看类型匹配。 字段是什么类型,传参就用什么类型。不要依赖隐式转换。

那次迁移之后,我学到一个东西:不同数据库之间的差异,不只是在SQL语法上,更在底层的行为逻辑上。写过五年Oracle的人,真不一定能写好金仓的SQL。好在这些坑踩过一次之后,下次就知道怎么绕过去了。

相关推荐
用户8356290780511 小时前
如何使用 Python 为 PDF 添加和管理图层
后端·python
SimonKing1 小时前
那个号称"对标 Spring"的国产框架 Solon,到底行不行?
java·后端·程序员
霸道流氓气质2 小时前
SpringBoot中使用字典驱动的动态路由示例
java·spring boot·后端
用户69371750013842 小时前
Kimi K3 综合能力处于全球第一梯队
android·前端·后端
anno2 小时前
Agent 新手 Skill 优先级指南:从第一套必装工作流,到专业能力补全
前端·后端
anno2 小时前
别再把文档“一刀切”:用 LangChain.js 做好 RAG 的第一公里
前端·后端
用户713874229002 小时前
claude_rules 深度解析:Claude Code 的模块化路径作用域规则
后端
卡卡罗特AI2 小时前
曾经狂推的 Superpowers,今天我终于把它卸载了! 臃肿,Token吞金兽!
后端·chatgpt·ai编程
用户713874229003 小时前
深入理解 Claude Code 的 Skill 机制:可复用能力与上下文成本的平衡术
后端