Skip to content

表达式求值规则

在 PostgreSQL 中,表达式的求值规则对编写可靠查询至关重要。本文将详细介绍这些规则及其影响。

表达式求值顺序

PostgreSQL 中表达式的子表达式求值顺序是**未定义的**。这意味着运算符或函数的输入参数不一定按照从左到右或任何固定顺序进行求值。

这与许多编程语言中的行为不同,在那些语言中通常有明确的求值顺序(如从左到右)。

短路求值的特殊性

PostgreSQL 的布尔运算中可能会进行"短路求值",但方式并不固定。如果部分表达式的结果足以确定整个表达式的值,则其他部分可能不会被求值。

INFO

短路求值当系统能够通过仅计算部分表达式就能确定整个表达式的结果时,便可能跳过剩余部分的计算。

示例

sql
SELECT true OR somefunc();

在这个例子中,由于 true OR 任何值 的结果必定是 true,因此 somefunc() 可能根本不会被调用。

同样地,即使函数调用放在前面:

sql
SELECT somefunc() OR true;

somefunc() 也可能不会执行,这取决于查询优化器的决定。

这种行为与某些编程语言(如 Java、C++)中从左到右进行"短路"求值的逻辑运算符不同。在 PostgreSQL 中,求值顺序可以被优化器自由调整。

避免依赖副作用

由于求值顺序的不确定性,强烈建议不要在复杂表达式中使用带有副作用的函数(即那些除了返回值外还会改变数据库状态的函数)。

特别危险的是在 WHEREHAVING 子句中依赖于副作用或特定的评估顺序。这些子句会作为执行计划优化的一部分被广泛重组,优化器可能会按照布尔代数规则允许的任何方式重新排列布尔表达式(AND/OR/NOT 组合)。

确保安全求值的解决方案

使用 CASE 构造控制求值顺序

当必须强制特定的求值顺序时,可以使用 CASE 构造:

不安全的除法示例

sql
-- 不安全: 如果 x = 0,会发生除零错误
SELECT ... WHERE x > 0 AND y/x > 1.5;

使用 CASE 构造保证安全

sql
-- 安全: 只有在 x > 0 时才会计算 y/x
SELECT ... WHERE CASE WHEN x > 0 THEN y/x > 1.5 ELSE false END;

更好的解决方案在上面的例子中,更好的方法是重写表达式以避免除法:

sql
SELECT ... WHERE x > 0 AND y > 1.5*x;

这样既安全又高效。

CASE 构造的局限性

使用 CASE 并不能解决所有问题:

  1. 不能阻止常量子表达式的早期求值:标记为 IMMUTABLE 的函数和运算符可能在查询计划阶段(而非执行阶段)就被求值。

  2. 不能防止聚合表达式的并行求值:聚合表达式会在考虑其他表达式之前进行计算。

常量子表达式的早期求值问题

sql
-- 危险: 可能在计划阶段就计算 1/0,即使永远不会进入 ELSE 分支
SELECT CASE WHEN x > 0 THEN x ELSE 1/0 END FROM tab;

即使表中的每一行 x > 0 都成立(这意味着在运行时永远不会执行 ELSE 分支),查询计划阶段仍可能会尝试简化 1/0 这个常量表达式,从而导致除零错误。

这个问题在函数内执行的查询中可能不那么明显,因为函数参数和局部变量可能作为常量被插入到查询中用于规划目的。

更可靠的解决方案

在 PL/pgSQL 函数中,使用 IF-THEN-ELSE 语句比 CASE 表达式更安全:

sql
-- PL/pgSQL 函数中的安全写法
IF x > 0 THEN
  RETURN x;
ELSE
  -- 永远不会执行到这里的危险代码
  RETURN 1/0;
END IF;

聚合表达式的求值问题

CASE 也无法阻止其内部聚合表达式的求值,因为聚合表达式会在其他表达式之前计算。

sql
-- 危险: 如果任何部门的 employees = 0,仍会发生除零错误
SELECT CASE WHEN min(employees) > 0
            THEN avg(expenses / employees)
       END
FROM departments;

在这个例子中,min()avg() 聚合函数会并发地处理所有输入行。如果任何行的 employees 等于零,会在测试 min() 结果之前就发生除零错误。

正确的解决方案

使用 WHERE 或 FILTER 子句来过滤有问题的行:

sql
-- 安全: 先过滤掉 employees = 0 的行
SELECT avg(expenses / employees)
FROM departments
WHERE employees > 0;

-- 或使用 FILTER 子句
SELECT avg(expenses / employees) FILTER (WHERE employees > 0)
FROM departments;

表达式求值流程图

安全实践总结

为了避免由于表达式求值规则导致的问题:

安全实践描述例子
避免依赖副作用不要依赖函数调用的顺序或副作用避免在表达式中使用修改数据的函数
重写表达式尽可能重写表达式避免危险操作y > 1.5*x 替代 y/x > 1.5
使用 CASE控制简单情况下的求值顺序CASE WHEN x > 0 THEN y/x ELSE NULL END
使用 WHERE/FILTER在聚合前过滤问题数据avg(x) FILTER (WHERE y <> 0)
在 PL/pgSQL 中使用 IF比 CASE 更安全的流程控制IF-THEN-ELSE 结构

在编写复杂 SQL 查询时,始终假设优化器可能会以任何顺序安排表达式的求值。编写不依赖于特定求值顺序的代码才是最安全的。

特别注意处理可能导致除零、NULL 引用或其他运行时错误的表达式。总是使用适当的防御性编程技术,如先验证条件再执行敏感操作。