KESvsOracle:90%开发者都踩过的WHERE执行顺序坑
2026/5/9 1:48:30 网站建设 项目流程

KESvsOracle:90%开发者都踩过的WHERE执行顺序坑

在 WHERE 子句里放一个"有副作用"的函数,就像在高速公路上放了一个随机变道的司机——也许今天没事,但迟早会出事故。

引言:一段看起来"理所当然"的代码

在一次代码评审中,我看到了这样一条 SQL:

SELECT*FROMemployeesWHEREget_department_id()=set_department_id('IT')+0;

编写者的意图很明确:先调用set_department_id('IT')设置一个全局变量,然后调用get_department_id()读取它,用这个值去过滤employees表。

他的理由是:“在 KES 里,WHERE 子句是从左到右执行的,所以set一定先于get执行,没问题。”

听起来有道理。但作为经历过线上事故的 DBA,我背后的冷汗瞬间就下来了。

这段代码有三个致命问题:

  1. 它依赖于函数的执行顺序
  2. 它依赖于函数的副作用(修改全局状态)
  3. 它假设了数据库版本的行为一致性

本文将深入解析为什么在WHERE子句中依赖函数执行顺序是不安全的,以及 KES 和 Oracle 在这个问题上的不同处理方式。

一、核心问题:WHERE 中的函数执行顺序到底确不确定?

1.1 Oracle 的不确定性

在 Oracle 中,WHERE子句中多个函数的执行顺序没有保证

虽然通常从左到右执行,但 Oracle 优化器可能基于以下原因调整执行顺序:

  • 谓词重排(Predicate Reordering):优化器根据过滤率和代价,重新排列WHERE条件中各表达式的求值顺序,以尽早过滤掉不满足条件的行
  • 短路优化:如果一个条件已经能确定整个WHERE表达式的真假,优化器可能跳过其他条件
  • 并行执行:在并行查询中,不同片段可能在不同线程上以不同顺序执行

这意味着:今天从左到右执行的代码,明天换个执行计划可能就从右到左了

1.2 KES 的确定性路径

金仓数据库 KES 在这个问题上采取了更为确定的策略:

KES 严格按 WHERE 子句中表达式的书写顺序,从左到右依次执行(无论等式还是不等式)。

这一设计降低了开发者的认知负担——你写的顺序就是执行顺序。但请注意:确定性不等于安全性

为什么?因为:

  1. KES 未来版本可能引入谓词重排优化(其他主流数据库都有这个特性)
  2. 即使当前版本确定,依赖执行顺序的代码也缺乏可移植性

1.3 对比总结

维度OracleKES(当前版本)
执行顺序保证不保证,优化器可能重排保证,严格从左到右
谓词重排支持当前不支持
未来变更风险高(行为已不确定)中(未来可能引入重排)
跨版本可移植性差(不建议依赖此行为)

结论:无论在哪种数据库中,依赖WHERE子句中的函数执行顺序都是不安全的做法。

二、为什么这种做法如此危险?

2.1 会话污染:全局变量的定时炸弹

让我们回到文章开头的例子:

SELECT*FROMemployeesWHEREget_department_id()=set_department_id('IT')+0;

假设这段代码在开发环境中"正常工作"了。问题出在生产环境:

场景 1:连接池复用

生产环境使用连接池。连接被归还给连接池后,set_department_id设置的会话级变量不会被清除。下一个复用该连接的查询,可能读到的是上一个查询残留的值。

连接 1: set_department_id('IT') → 查询 → 归还连接池 (会话变量仍为 'IT') 连接 2: 复用连接 1 → get_department_id() → 读到 'IT' (但连接 2 的本意是查 'HR')

结果:查询返回了错误的数据,且没有任何报错。这种静默错误是最难排查的。

场景 2:并发查询

多个并发会话同时调用set_department_id,全局变量被互相覆盖。在高并发场景下,查询结果变得不可预测。

2.2 优化器重写的潜在风险

即使 KES 当前版本保证从左到右执行,但这不意味着未来不会改变。数据库优化器的发展方向是越来越智能——谓词重排是提升查询性能的标准技术之一。

如果未来 KES 版本引入了谓词重排优化,这段代码的执行顺序可能突然改变,导致:

  • getset之前执行 → 读到旧值 → 查询结果错误
  • 没有任何版本升级警告或错误提示

这种静默行为变更是生产环境中最危险的问题类型。

2.3 函数挥发度(Volatility)的影响

数据库中的函数通常有一个挥发度标记(Volatility),用于告知优化器函数的行为特征:

挥发度含义优化器行为
IMMUTABLE相同输入永远返回相同输出,无副作用可以缓存结果、提前求值
STABLE同一事务内相同输入返回相同输出可在事务内缓存
VOLATILE每次调用可能返回不同结果,或有副作用必须每次求值,不可优化

如果函数没有正确声明挥发度(默认通常是VOLATILE),优化器可能做出错误的优化决策。反之,如果将有副作用的函数错误声明为STABLEIMMUTABLE,优化器可能缓存结果或跳过调用,导致副作用不被执行。

三、解决方案:如何安全地处理"先 Set 后 Get"的需求

3.1 方案一:通过存储过程显式完成 Set 操作(推荐)

将有副作用的操作从 SQL 表达式中剥离,在存储过程或匿名块中显式执行:

-- KES PL/SQL 匿名块BEGINset_department_id('IT');-- 设置完成后,再执行查询FORrecIN(SELECT*FROMemployeesWHEREdept_id=get_department_id())LOOP-- 处理结果ENDLOOP;END;/

这种方式的优势:

  • 执行顺序显式可控——BEGINEND之间的语句严格按书写顺序执行
  • 副作用与查询分离——避免了在表达式中嵌入有副作用的调用
  • 可读性更好——代码意图一目了然

3.2 方案二:通过参数传递,避免全局状态

如果你只是想传递一个过滤值给查询,最直接的方式是用参数:

-- 在应用层设置参数PREPAREstmtASSELECT*FROMemployeesWHEREdept_id=$1;EXECUTEstmt('IT');

或者在存储过程中:

CREATEORREPLACEPROCEDUREquery_by_dept(p_dept_idVARCHAR)ASBEGINFORrecIN(SELECT*FROMemployeesWHEREdept_id=p_dept_id)LOOP-- 处理结果ENDLOOP;END;/

用参数替代全局变量,从根本上消除了会话污染的风险。

3.3 方案三:正确声明函数挥发度

对于纯读取、无副作用的函数,务必声明正确的挥发度:

-- 纯读取函数:声明为 STABLECREATEORREPLACEFUNCTIONget_department_name(dept_idINTEGER)RETURNSVARCHARSTABLE-- 告知优化器:同一事务内,相同输入返回相同输出AS$$SELECTdept_nameFROMdepartmentsWHEREid=$1;$$LANGUAGESQL;-- 计算函数:声明为 IMMUTABLECREATEORREPLACEFUNCTIONcalculate_bonus(salaryNUMERIC)RETURNSNUMERICIMMUTABLE-- 告知优化器:相同输入永远返回相同输出AS$$SELECTsalary*0.1;$$LANGUAGESQL;

正确的挥发度声明能帮助优化器做出更好的决策,同时避免对有副作用的函数进行不当优化。

3.4 方案四:使用 WITH 子句确保执行顺序

在 KES 中,WITH子句(CTE)可以保证内部语句的执行顺序。虽然这不是标准 SQL 的语义保证,但 KES 当前版本中 CTE 不会被内联优化:

WITHsetupAS(SELECTset_department_id('IT')ASresult)SELECT*FROMemployees,setupWHEREdept_id=get_department_id();

注意:这种方式依赖于 KES 的 CTE 实现细节,未来版本如果引入 CTE 内联优化,行为可能改变。因此仅作为临时方案,不作为长期推荐。

四、铁律总结

以下是你在数据库开发中应该牢记的几条铁律:

  1. 严禁在 WHERE 中放置有"副作用"的函数——包括但不限于修改全局变量、写日志、发送消息、修改表数据等。
  2. 通过存储过程或匿名块显式完成 Set 操作——将副作用操作与查询分离,确保执行顺序可控。
  3. 纯读取函数声明为 STABLE 或 IMMUTABLE——帮助优化器正确决策,避免不必要的重复调用。
  4. 永远不要假设 WHERE 子句的执行顺序——即使在当前版本中是确定的,也不代表未来版本或其他数据库中保持一致。
  5. 用参数替代全局变量——在连接池环境下,全局变量是定时炸弹。

总结

WHERE子句中依赖函数执行顺序,是一种看似工作、迟早爆炸的反模式。

KES 当前版本虽然保证了从左到右的执行顺序,但这不应成为你编写依赖此行为代码的理由。原因有三:

  1. 会话污染:连接池环境下的全局变量残留会导致静默数据错误
  2. 未来风险:优化器升级可能引入谓词重排,改变执行顺序
  3. 可移植性:依赖特定数据库实现细节的代码无法跨库迁移

正确的做法是:将有副作用的操作从 SQL 表达式中剥离,通过存储过程、参数传递或正确的函数挥发度声明来替代。简洁、显式、可预测——这是所有优秀数据库代码的共同特征。


本文基于金仓数据库 KingbaseES V9 / Oracle 19c 编写。函数挥发度说明参考 PostgreSQL / KES 函数定义规范。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询