一条SQL能执行成功,不代表它真的安全:一次生产环境数据库迁移事故复盘

一条SQL能执行成功,不代表它真的安全:一次生产环境数据库迁移事故复盘

一个"偶尔查不到数据"的问题,最后查到了SQL执行顺序

去年做一个国产数据库迁移项目的时候,遇到过一个特别难排查的问题。

上线前所有测试都通过了,功能也验收完成。

结果上线几天后,业务反馈:

"这个页面偶尔加载不出来,但是刷新一下又好了。"

一开始大家第一反应都是应用层的问题。

是不是缓存?

是不是连接池?

是不是某个服务节点状态不一致?

因为这个问题出现得没有规律,所以排查起来特别痛苦。

我们先清缓存,没效果。

然后怀疑连接池复用了异常会话,重启应用试了一遍,问题依然存在。

直到后来把问题缩小到数据库查询,才发现事情没有那么简单。

最奇怪的是:

同一个用户,同一条SQL,在同一个数据库里连续执行两次,第一次没有结果,第二次却能查出来。

这时候基本可以确定,不是数据问题,而是SQL本身存在隐藏状态。


一条看起来没问题的SQL

最后定位到了一条类似这样的SQL:

sql 复制代码
SELECT *
FROM orders
WHERE id1 = pkg_order.get_id()
AND pkg_order.set_user(10) = 1;

很多开发看到这里可能会觉得:

"这有什么问题?"

当时写这段SQL的人也是这么想的。

他的逻辑很简单:

  1. 调用 set_user(10) 设置当前用户上下文;
  2. 再通过 get_id() 获取用户ID;
  3. 根据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优化、国产数据库迁移等方向感兴趣,欢迎参与金仓社区交流。

目前社区正在开展:

  1. 荐商机·赢好礼------金仓社区"同行者计划"

分享数据库实践经验、推荐解决方案,有机会获得奖励:

bbs.kingbase.com.cn/forumDetail...

  1. 2026金仓数据库智能运维工具开发大赛

面向数据库工具开发、自动化运维方向,欢迎报名参与:

bbs.kingbase.com.cn/forumDetail...

  1. 数据库技术征文活动

欢迎分享使用金仓数据库过程中的实践经验、优化技巧和迁移案例:

bbs.kingbase.com.cn/web-api/for...

相关推荐
Reart18 小时前
Leetcode 1143.最长公共子序列(720)
后端·算法
Reart18 小时前
Leetcode 718.最长重复子数组(720)
后端
云上小朱18 小时前
软件更新-openssh和openssl-centos
后端
Conan在掘金18 小时前
鸿蒙 ArkUI 深水区:Navigation 多级路由,从「拼页面」到「搭应用」的分水岭
后端
千纸鹤安安18 小时前
如何看待 Go 1.18 引入的泛型?对 Go 开发者来说是必须掌握的吗?
后端
颜进强18 小时前
Claude Code -21 Agent 规划化编写范式
前端·后端
wing9819 小时前
通往全干之路之:被迫成为全栈
前端·后端·程序员
云上小朱20 小时前
在k8s部署alist 3.62.0
后端
花开彼岸天~20 小时前
鸿蒙原生开发手记:徒步迹 - 轨迹回放动画实现
后端