WHERE 子句里的“暗雷“:当函数副作用撞上数据库优化器

在 WHERE 条件里塞一个有副作用的函数,就像在代码里埋了一颗延时引信------今天测没问题,上线后随时可能炸。


引子:一个"看似合理"的陷阱

某次代码评审,我盯着这条 SQL 愣了三秒:

sql 复制代码
SELECT * FROM employees
WHERE get_department_id() = set_department_id('IT') + 0;

写这段代码的同事振振有词:"KES 的 WHERE 是从左往右跑的,set 肯定先于 get 执行,稳得很。"

我后背一凉。

这段代码犯了三个致命忌讳:

  1. 赌执行顺序------把业务正确性押在数据库的求值顺序上
  2. 玩副作用------在查询里偷偷改全局状态
  3. 假设行为永恒------认为当前版本的表现就是未来的契约

本文将拆解:为什么在 WHERE 里依赖函数调用顺序是颗定时炸弹,以及 KES 和 Oracle 对此的不同态度。


一、核心矛盾:WHERE 里的函数,谁先谁后?

1.1 Oracle:优化器的"自由裁量权"

在 Oracle 地盘上,WHERE 子句里多个函数的调用顺序没有契约保障

虽然大概率从左到右走,但优化器随时可能"重新洗牌":

  • 谓词重排(Predicate Reordering):优化器按过滤率和代价模型重新排布条件,尽早踢掉不合格的行
  • 短路求值:某个条件已能定生死,后面的可能直接跳过
  • 并行切片:并行查询时,不同分片可能在不同线程上各跑各的

翻译成人话:今天从左到右的代码,明天执行计划一变就可能从右到左

1.2 KES:当下的"确定性承诺"

金仓数据库 KES 目前走了一条更"老实"的路:

KES 严格遵循 WHERE 子句的书写顺序,从左到右依次求值(无论等式还是不等式)。

这对开发者很友好------你写的顺序就是跑的顺序。但记住:确定性 ≠ 安全性

原因很现实:

  1. KES 后续版本完全可能引入谓词重排(这是行业大势)
  2. 即便现在确定,这种写法也跨不了库、迁不了版本

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;
/

好处:

  • 顺序肉眼可见 ------BEGINEND 之间严格按书写顺序走
  • 副作用与查询解耦------不再在表达式里藏"暗器"
  • 可读性碾压------意图一目了然

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 内联优化,行为可能变。所以只当临时过渡,别当长期方案。


四、铁律:数据库开发的四条红线

  1. WHERE 里禁放"带副作用"的函数------包括但不限于改全局变量、写日志、发消息、改表数据。
  2. 副作用操作明着来------用存储过程或匿名块显式执行,与查询分离。
  3. 纯函数标对挥发度 ------STABLEIMMUTABLE,帮优化器也帮自己。
  4. 别赌 WHERE 的执行顺序------即便现在确定,也不代表未来或其他数据库一样。
  5. 参数优于全局变量------连接池环境下,全局变量就是幽灵。

结语

WHERE 里依赖函数执行顺序,是一种今天能跑、明天爆炸的典型反模式。

KES 现行版本虽然保证了从左到右,但这不该成为你写依赖代码的借口。原因有三:

  1. 会话污染:连接池里的残留变量会导致静默数据错误
  2. 未来风险:优化器升级可能引入谓词重排,顺序说变就变
  3. 可移植性:绑死在特定数据库实现上的代码,迁不动

正确的姿势:把副作用从 SQL 表达式里剥离,用存储过程、参数传递、正确的挥发度声明来替代。干净、显式、可预期------这才是好数据库代码的底色。


相关推荐
容智信息1 小时前
不写SQL,不拉Excel:数据分析用“问”的
数据库·人工智能·笔记·数据分析·excel·知识图谱·知识库
2401_898717661 小时前
如何在 SvelteKit 中为动态加载的图片正确实现悬停显示覆盖层
jvm·数据库·python
HalvmånEver1 小时前
MySQL视图
linux·数据库·学习·mysql·视图
m0_702036531 小时前
如何在MongoDB中实现按时间跨度的分片路由_时间序列范围分片与冷热节点架构.txt
jvm·数据库·python
2301_808414381 小时前
MySQL表的增删查改
数据库·mysql
2401_884454151 小时前
golang如何使用Fiber高性能框架_golang Fiber框架入门教程
jvm·数据库·python
ㄟ留恋さ寂寞1 小时前
Golang怎么读取和修改图片EXIF信息_Golang如何用goexif提取照片的拍摄时间和GPS位置【方法】
jvm·数据库·python
zhoutongsheng1 小时前
如何在 SvelteKit 中为动态加载的图片正确实现悬停显示覆盖层
jvm·数据库·python
a7963lin1 小时前
Go语言怎么做分布式缓存_Go语言分布式缓存教程【经典】
jvm·数据库·python