每个函数都具有一个易失性(volatility)分类,可选值为 VOLATILE、STABLE 或 IMMUTABLE。如果在 CREATE FUNCTION 命令中未显式指定分类,则默认为 VOLATILE。易失性分类是对优化器关于函数行为的一种承诺:
VOLATILE(易失)函数可以执行任何操作,包括修改数据库。即使传入相同的实参,连续调用也可能返回不同的结果。优化器对此类函数的行为不作任何假设。使用易失函数的查询会在需要其值的每一个数据行上重新对其进行求值。
STABLE(稳定)函数不能修改数据库,并且保证在单条语句执行期间对于所有数据行,给定相同的实参必然返回相同的结果。该分类允许优化器将对该函数的多次重复调用优化为单次调用。特别是,在索引扫描条件中包含此类函数的表达式是完全安全的(因为索引扫描只会计算一次比较值而非逐行求值,所以在索引扫描条件中使用 VOLATILE 函数是非法的)。
IMMUTABLE(不可变)函数不能修改数据库,并且保证只要给定相同的实参,永远返回相同的结果。该分类允许优化器在查询使用常量参数调用该函数时提前对其进行常量折叠求值。例如诸如 SELECT ... WHERE x = 2 + 2 的查询可以直接简化为 SELECT ... WHERE x = 4,因为整数加法运算符底层的函数被标记为了 IMMUTABLE。
为了获得最佳的优化效果,您应当为函数标记其所适用的最严格的易失性分类。
任何带有副作用的函数都必须标记为 VOLATILE,这样对它的调用才不会被优化消除。即使是没有副作用的函数,如果其返回值在单条查询的执行过程中可能发生改变,也需要标记为 VOLATILE;典型的例子包括 random()、currval() 和 timeofday()。
另一个重要的例子是 current_timestamp 系列函数属于 STABLE,因为它们的值在单个事务内部是保持不变的。
在考虑即时生成计划并立刻执行的简单交互式查询时,STABLE 和 IMMUTABLE 分类之间的差异相对较小:函数是在规划期间执行一次还是在查询执行启动时执行一次影响不大。但如果执行计划被缓存并在稍后重用,二者的差异就会变得巨大。如果将实际上不是不可变的函数错误标记为 IMMUTABLE,可能会导致其在规划期间过早折叠为常量,从而在后续重复使用该计划时复用过期的旧值。在使用预备语句(prepared statements)或缓存计划的编程语言(如 PL/pgSQL)时,这构成严重的隐患。
对于使用 SQL 或任何标准过程语言编写的函数,易失性分类还决定了第二个重要属性:即由调用该函数的当前 SQL 命令所作出的任何数据修改对函数是否可见。VOLATILE 函数能够看到这些修改,而 STABLE 或 IMMUTABLE 函数则看不到。这一行为基于 MVCC 的快照机制实现(参见 Chapter 13):STABLE 和 IMMUTABLE 函数使用在外层调用查询启动时建立的快照,而 VOLATILE 函数在每次执行查询时都会获取一个全新的快照。
使用 C 语言编写的函数可以按需自主管理快照,但通常遵循上述行为模式也是良好的实践。
基于这种快照行为,仅包含 SELECT 命令的函数可以安全地标记为 STABLE,即使它所查询的表可能正被并发事务修改。PostgreSQL 将使用为外层调用查询建立的快照来执行 STABLE 函数中的所有命令,因此在整个查询执行期间它看到的都是数据库的一致性固定视图。
IMMUTABLE 函数内部的 SELECT 命令也使用相同的快照机制。通常在 IMMUTABLE 函数中查询数据库表是极不明智的,因为一旦表内容发生变化,函数的不可变性承诺就会被打破。不过,PostgreSQL 并不在语法上强制禁止这种写法。
一个常见的错误是将计算结果依赖于配置参数的函数错误标记为 IMMUTABLE。例如,处理时间戳的函数其结果很可能依赖于 TimeZone 设置。为了安全起见,此类函数应当标记为 STABLE。
PostgreSQL 要求 STABLE 和 IMMUTABLE 函数中除 SELECT 外不得包含其他修改数据的 SQL 命令(这并非完全绝对的安全屏障,因为此类函数仍可能间接调用修改数据库的 VOLATILE 函数。如果这样做,您会发现 STABLE 或 IMMUTABLE 函数无法感知被调用函数所做的数据库修改,因为这些修改对其快照是隐藏的)。