在 WHERE 条件里塞一个有副作用的函数,就像在代码里埋了一颗延时引信------今天测没问题,上线后随时可能炸。
引子:一个"看似合理"的陷阱
某次代码评审,我盯着这条 SQL 愣了三秒:
sql
SELECT * FROM employees
WHERE get_department_id() = set_department_id('IT') + 0;
写这段代码的同事振振有词:"KES 的 WHERE 是从左往右跑的,set 肯定先于 get 执行,稳得很。"
我后背一凉。
这段代码犯了三个致命忌讳:
- 赌执行顺序------把业务正确性押在数据库的求值顺序上
- 玩副作用------在查询里偷偷改全局状态
- 假设行为永恒------认为当前版本的表现就是未来的契约
本文将拆解:为什么在 WHERE 里依赖函数调用顺序是颗定时炸弹,以及 KES 和 Oracle 对此的不同态度。
一、核心矛盾:WHERE 里的函数,谁先谁后?
1.1 Oracle:优化器的"自由裁量权"
在 Oracle 地盘上,WHERE 子句里多个函数的调用顺序没有契约保障。
虽然大概率从左到右走,但优化器随时可能"重新洗牌":
- 谓词重排(Predicate Reordering):优化器按过滤率和代价模型重新排布条件,尽早踢掉不合格的行
- 短路求值:某个条件已能定生死,后面的可能直接跳过
- 并行切片:并行查询时,不同分片可能在不同线程上各跑各的
翻译成人话:今天从左到右的代码,明天执行计划一变就可能从右到左。
1.2 KES:当下的"确定性承诺"
金仓数据库 KES 目前走了一条更"老实"的路:
KES 严格遵循 WHERE 子句的书写顺序,从左到右依次求值(无论等式还是不等式)。
这对开发者很友好------你写的顺序就是跑的顺序。但记住:确定性 ≠ 安全性。
原因很现实:
- KES 后续版本完全可能引入谓词重排(这是行业大势)
- 即便现在确定,这种写法也跨不了库、迁不了版本
1.3 两家对比
| 维度 | Oracle | KES(现行版本) |
|---|---|---|
| 顺序保障 | 不承诺,优化器说了算 | 承诺,严格从左到右 |
| 谓词重排 | 已支持 | 暂不支持 |
| 版本升级风险 | 高(本身就不确定) | 中(未来可能变) |
| 跨库可移植性 | 差 | 差(别依赖这个) |
核心结论 :无论在哪款数据库里,在 WHERE 里赌函数执行顺序都是高危操作。

二、这颗雷为什么特别响?
2.1 会话污染:连接池里的"幽灵变量"
回到开头那个例子:
sql
SELECT * FROM employees WHERE get_department_id() = set_department_id('IT') + 0;
开发环境跑得好好的,一上生产就诡异:
场景一:连接池"借尸还魂"
生产环境用连接池。连接还回去后,set_department_id 设置的会话变量不会自动清零。下一个拿到这条连接的查询,读到的可能是上家留下的"遗产"。
连接 A: set_department_id('IT') → 查完 → 还池
(会话变量残留 'IT')
连接 B: 复用连接 A → get_department_id() → 读到 'IT'
(但 B 本来要查 'HR')
结果:数据错了,全程没报错 。这种静默错误是最难啃的骨头。

场景二:并发互踩
多个会话同时 set_department_id,全局变量被来回覆盖。高并发下,查询结果跟抽奖一样。
2.2 优化器"翻脸"的风险
即便 KES 现在老实从左到右,不代表永远老实。数据库优化器的进化方向是越来越聪明------谓词重排是性能优化的标配手段。
万一未来 KES 引入了这个能力,这段代码的执行顺序可能突然翻转:
get跑到set前面 → 读到旧值 → 结果全错- 版本升级日志里不会提这个行为变更
这种无声无息的行为漂移,是生产环境最恐怖的噩梦。
2.3 函数挥发度(Volatility)的误导
数据库函数通常带个挥发度标签,告诉优化器这函数的"脾气":
| 挥发度 | 含义 | 优化器会怎么干 |
|---|---|---|
IMMUTABLE |
相同输入永远相同输出,无副作用 | 缓存结果、提前求值 |
STABLE |
同一事务内相同输入相同输出 | 事务内缓存 |
VOLATILE |
每次调用可能不同,或有副作用 | 每次必求值,不优化 |
如果挥发度标错了------比如把有副作用的函数标成 IMMUTABLE------优化器可能直接缓存结果或跳过调用,副作用就"消失"了。
三、拆弹指南:如何安全地"先设后取"

3.1 方案一:存储过程里明着来(首推)
把副作用操作从 SQL 表达式里拽出来,在存储过程或匿名块里显式执行:
sql
-- KES PL/SQL 匿名块
BEGIN
set_department_id('IT');
-- 设完再查,顺序肉眼可见
FOR rec IN (SELECT * FROM employees WHERE dept_id = get_department_id()) LOOP
-- 处理结果
END LOOP;
END;
/
好处:
- 顺序肉眼可见 ------
BEGIN到END之间严格按书写顺序走 - 副作用与查询解耦------不再在表达式里藏"暗器"
- 可读性碾压------意图一目了然
3.2 方案二:传参,别用全局变量

如果只是想传个过滤值,最直接的是参数:
sql
-- 应用层设参
PREPARE stmt AS SELECT * FROM employees WHERE dept_id = $1;
EXECUTE stmt('IT');
或者存过里:
sql
CREATE OR REPLACE PROCEDURE query_by_dept(p_dept_id VARCHAR) AS
BEGIN
FOR rec IN (SELECT * FROM employees WHERE dept_id = p_dept_id) LOOP
-- 处理结果
END LOOP;
END;
/
用参数替代全局变量,从根本上拔掉会话污染的引信。
3.3 方案三:给函数贴对"标签"
纯读取、无副作用的函数,务必标对挥发度:
sql
-- 纯读取函数:标 STABLE
CREATE OR REPLACE FUNCTION get_department_name(dept_id INTEGER)
RETURNS VARCHAR
STABLE -- 告诉优化器:同一事务内,相同输入返回相同输出
AS $$
SELECT dept_name FROM departments WHERE id = $1;
$$ LANGUAGE SQL;
-- 计算函数:标 IMMUTABLE
CREATE OR REPLACE FUNCTION calculate_bonus(salary NUMERIC)
RETURNS NUMERIC
IMMUTABLE -- 告诉优化器:相同输入永远相同输出
AS $$
SELECT salary * 0.1;
$$ LANGUAGE SQL;
正确的挥发度声明能帮优化器做正确决策,也能防止它对有副作用的函数"乱优化"。
3.4 方案四:CTE 作为临时盾牌
在 KES 中,WITH 子句(CTE)目前能保证内部语句的执行顺序(CTE 暂不会被内联优化):
sql
WITH setup AS (
SELECT set_department_id('IT') AS result
)
SELECT * FROM employees, setup
WHERE dept_id = get_department_id();
注意:这依赖 KES 当前的 CTE 实现细节,未来如果引入 CTE 内联优化,行为可能变。所以只当临时过渡,别当长期方案。

四、铁律:数据库开发的四条红线
- WHERE 里禁放"带副作用"的函数------包括但不限于改全局变量、写日志、发消息、改表数据。
- 副作用操作明着来------用存储过程或匿名块显式执行,与查询分离。
- 纯函数标对挥发度 ------
STABLE或IMMUTABLE,帮优化器也帮自己。 - 别赌 WHERE 的执行顺序------即便现在确定,也不代表未来或其他数据库一样。
- 参数优于全局变量------连接池环境下,全局变量就是幽灵。
结语
在 WHERE 里依赖函数执行顺序,是一种今天能跑、明天爆炸的典型反模式。
KES 现行版本虽然保证了从左到右,但这不该成为你写依赖代码的借口。原因有三:
- 会话污染:连接池里的残留变量会导致静默数据错误
- 未来风险:优化器升级可能引入谓词重排,顺序说变就变
- 可移植性:绑死在特定数据库实现上的代码,迁不动
正确的姿势:把副作用从 SQL 表达式里剥离,用存储过程、参数传递、正确的挥发度声明来替代。干净、显式、可预期------这才是好数据库代码的底色。
