一条SQL能执行成功,不代表它真的安全:一次生产环境数据库迁移事故复盘
一个"偶尔查不到数据"的问题,最后查到了SQL执行顺序
去年做一个国产数据库迁移项目的时候,遇到过一个特别难排查的问题。
上线前所有测试都通过了,功能也验收完成。
结果上线几天后,业务反馈:
"这个页面偶尔加载不出来,但是刷新一下又好了。"
一开始大家第一反应都是应用层的问题。
是不是缓存?
是不是连接池?
是不是某个服务节点状态不一致?
因为这个问题出现得没有规律,所以排查起来特别痛苦。
我们先清缓存,没效果。
然后怀疑连接池复用了异常会话,重启应用试了一遍,问题依然存在。
直到后来把问题缩小到数据库查询,才发现事情没有那么简单。
最奇怪的是:
同一个用户,同一条SQL,在同一个数据库里连续执行两次,第一次没有结果,第二次却能查出来。
这时候基本可以确定,不是数据问题,而是SQL本身存在隐藏状态。
一条看起来没问题的SQL

最后定位到了一条类似这样的SQL:
sql
SELECT *
FROM orders
WHERE id1 = pkg_order.get_id()
AND pkg_order.set_user(10) = 1;
很多开发看到这里可能会觉得:
"这有什么问题?"
当时写这段SQL的人也是这么想的。
他的逻辑很简单:
- 调用
set_user(10)设置当前用户上下文; - 再通过
get_id()获取用户ID; - 根据ID查询订单。
从业务逻辑看,没有毛病。
甚至在原来的Oracle环境里,这条SQL已经稳定运行了几年。
问题就在于:
它偷偷依赖了WHERE条件的执行顺序。
SQL不是按照你写的顺序执行
很多刚接触数据库的人会有一个误区:
SQL写成:
sql
WHERE A
AND B
是不是数据库就一定先执行A,再执行B?
实际上不是。
数据库真正执行SQL之前,会经过优化器。
优化器关注的是:
- 哪个条件过滤效果更好;
- 哪个条件成本更低;
- 哪个条件可以利用索引;
- 怎么调整执行计划效率最高。
它并不知道你写这两个条件是因为业务上存在先后关系。
对于优化器来说:
sql
pkg_order.set_user(10) = 1
只是一个表达式。
如果它判断这个表达式计算成本低,完全可能提前执行。
于是可能出现:
情况一:
先执行:
sql
pkg_order.set_user(10)
变量被赋值。
然后:
sql
pkg_order.get_id()
可以正常返回。
SQL成功。
情况二:
优化器调整了执行方式。
先执行:
sql
pkg_order.get_id()
但是此时变量还没有初始化。
结果:
text
NULL
条件:
sql
id1 = NULL
自然查不到数据。
而后面的:
sql
pkg_order.set_user(10)
可能根本没有机会执行。
最终表现出来就是:
同一条SQL,有时候正常,有时候返回空。
最坑的是:它还会制造"假正常"
这种问题最恶心的地方,是它不是一直失败。
比如:
某个测试人员上午测试的时候:
先执行了一次初始化SQL。
当前Session里面已经有值。
然后测试查询:
正常。
大家觉得:
"没问题。"
但是换一个新的数据库连接:
Session是干净的。
变量没有初始化。
同样SQL:
失败。
所以这种问题经常出现:
- 测试环境没问题;
- 开发环境没问题;
- 生产偶尔出现。
因为它依赖的是隐藏状态。
国产化迁移为什么更容易暴露这种问题?
后来复盘的时候发现,这类问题在数据库迁移项目里特别容易出现。
原因不是国产数据库"不兼容"。
而是不同数据库的优化器实现不同。
Oracle长期运行的SQL,开发人员可能已经习惯了某些行为。
比如:
- 条件计算顺序;
- 函数调用时机;
- 执行计划选择。
但是迁移到其他数据库以后:
优化器重新评估成本。
原来"碰巧成立"的事情,不一定继续成立。
数据库版本升级、统计信息变化、索引调整,都可能导致执行计划变化。
于是隐藏多年的问题突然暴露。

这种写法,其实项目里并不少见
后来我顺手检查了一遍代码仓库,发现类似写法还不少。
比如:
sql
SELECT *
FROM order_detail
WHERE status = pkg_order.get_status()
AND pkg_order.init_context('processed') = 1;
看起来像查询。
实际上里面混进了状态修改。
还有:
sql
SELECT *
FROM report_data
WHERE pkg_report.init_session(:user_id)=0
AND pkg_report.get_type()='DAILY';
第一个函数负责初始化。
第二个函数依赖初始化结果。
这种代码最大的问题:
SQL表面是查询,实际上偷偷承担了业务流程控制。
为什么WHERE里面不要调用有副作用的函数?
原因很简单:
SQL负责描述"我要什么数据"。
不应该负责:
- 修改状态;
- 设置变量;
- 初始化环境;
- 控制业务流程。
否则数据库优化器就可能成为你的隐形变量。
你以为:
css
执行A → 执行B → 查询
数据库看到的是:
一堆可以重新排列的表达式
两者完全不是一个概念。
后来项目里定了几个SQL规范
经历这个问题之后,我们重新整理了一遍SQL规范。
第一条:禁止WHERE里面执行状态修改函数
例如:
不要:
sql
SELECT *
FROM orders
WHERE id=get_id()
AND set_user(10)=1;
改成:
sql
CALL set_user(10);
SELECT *
FROM orders
WHERE id=get_id();
业务流程明确拆开。
不要让数据库猜你的意图。
第二条:查询函数必须保持无副作用
如果函数只是查询数据,就明确告诉数据库:
它不会修改任何东西。
例如:
sql
CREATE FUNCTION get_user_level(p_user_id INT)
RETURNS INT
STABLE
AS $$
BEGIN
RETURN (
SELECT level
FROM users
WHERE id=p_user_id
);
END;
$$ LANGUAGE plpgsql;
让优化器知道:
这个函数可以安全优化。
第三条:不要依赖Session变量保存业务状态
这是很多老系统里面的习惯。
比如:
text
当前用户ID存在包变量
当前权限存在Session变量
当前流程状态存在内存变量
短期看方便。
长期非常危险。
因为:
- 换连接可能丢失;
- 并发环境容易污染;
- 迁移数据库容易出问题。
业务数据应该存表。
临时数据应该明确生命周期。
不要让Session承担业务状态。
第四条:SQL上线前一定看执行计划
很多问题不是SQL语法错误。
而是执行计划变化。
建议上线前重点关注:
sql
EXPLAIN ANALYZE
SELECT *
FROM orders
WHERE status=1
AND amount>100;
重点看:
- Filter条件;
- 是否走索引;
- 函数执行次数;
- 实际扫描行数。
不要只看:
"能返回结果"。
后来的代码审查,我会重点盯这几类SQL
现在做代码Review,只要看到下面几种写法,我都会特别关注:
- WHERE里面调用自定义函数;
- 函数里面修改变量;
- SQL依赖Session状态;
- 多个函数之间存在调用顺序关系;
- 同一个查询在不同环境执行结果不同。
这些代码最大的特点就是:
开发阶段看起来完全正常,生产环境可能突然爆炸。
这次事故给我的最大感受是:
SQL能执行,只能说明语法正确。
真正可靠的SQL,需要满足:
- 不依赖偶然执行顺序;
- 不依赖隐藏状态;
- 不依赖某个数据库的特殊行为;
- 换环境之后依然稳定。
数据库优化器不是你的程序执行引擎。
不要把业务流程藏在SQL条件里面。
写SQL的时候少一点"聪明设计",多一点明确拆分,反而更稳定。
因为真正优秀的SQL,不是刚好能跑。
而是在任何执行计划、任何环境、任何数据库版本下,都还能稳定运行。
(以下社区活动宣传建议单独作为文章末尾"活动推荐"模块,不要和技术正文混在一起。)
金仓社区近期活动推荐
如果你对数据库开发规范、SQL优化、国产数据库迁移等方向感兴趣,欢迎参与金仓社区交流。
目前社区正在开展:
- 荐商机·赢好礼------金仓社区"同行者计划"
分享数据库实践经验、推荐解决方案,有机会获得奖励:
bbs.kingbase.com.cn/forumDetail...
- 2026金仓数据库智能运维工具开发大赛
面向数据库工具开发、自动化运维方向,欢迎报名参与:
bbs.kingbase.com.cn/forumDetail...
- 数据库技术征文活动
欢迎分享使用金仓数据库过程中的实践经验、优化技巧和迁移案例: