上线前能跑,上线后挂了,三个隐性的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。好在这些坑踩过一次之后,下次就知道怎么绕过去了。

相关推荐
步行cgn4 小时前
Spring 基于 XML 的自动装配:byName 详解
java·后端·spring
RISCV_Explorer6 小时前
RISC-V启动与运行时规范机制解析——BRS约定、HSM多核启动与设备树要求
后端·risc-v
星栈独行6 小时前
ADK-Rust 是什么?Rust 生态新一代 AI Agent 开发套件
人工智能·后端·rust
JavaGuide6 小时前
对标 MinIO!全新一代分布式文件系统正式发布
前端·后端
用户8356290780517 小时前
使用 Python 将 DOCX 转换为 Markdown
后端·python
IT_陈寒7 小时前
Vue computed属性这个坑,我居然踩了三次才爬出来
前端·人工智能·后端
孙启超8 小时前
【AI开发之Rust】第 7 课:错误处理 —— panic、Result 与 `?`
人工智能·分布式·后端·爬虫·spring cloud·架构·rust
烈风逍遥8 小时前
第一篇:SeaPack 全栈项目工程化实践
前端·后端·架构
烈风逍遥8 小时前
第二篇:SeaPack 权限体系:从"谁都能看"到"该看什么看什么"
前端·后端·架构
JoyT8 小时前
Spring AI 2.0 Agent 进阶:Memory、State 与 Context Engineering 常见技术全景
后端