PostgreSQL 的操作符定义可以包含若干可选 子句,用于告诉系统关于该操作符行为的有用信息。只要合适就应当提供这些 子句,因为它们能显著加速使用该操作符的查询的执行。但如果提供了这些 子句,就必须确保它们是正确的!错误地使用优化子句可能导致查询缓慢、 输出出现微妙错误或其他糟糕的后果。如果不确定,完全可以省略某个优化 子句;唯一的后果是查询的运行速度可能比本可以达到的慢。
未来版本的 PostgreSQL 可能会增加更多的 优化子句。这里描述的这些就是 18.6 版本所支持的全部内容。
还可以为操作符的底层函数附加一个规划器支持函数,这是告诉系统该操作符 行为的另一种方式。 更多信息见 Section 36.11。
COMMUTATOR #
如果提供了 COMMUTATOR 子句,它会指定一个操作符
作为被定义操作符的交换子。如果对所有可能的输入值 x、y 都有
(x A y) 等于 (y B x),我们就说操作符 A 是操作符 B 的交换子。
注意 B 也是 A 的交换子。例如,对于特定数据类型,操作符
< 和 > 通常互为交换子,
而操作符 + 通常与自身可交换。
但操作符 - 通常不与任何操作符可交换。
可交换操作符的左操作数类型与其交换子的右操作数类型相同,反之亦然。
因此 PostgreSQL 只需要交换子操作符的名字
就能查找交换子,COMMUTATOR 子句中也只需提供
操作符名。
对于将被用于索引和连接子句的操作符,提供交换子信息至关重要,因为它
让查询优化器能够把这样的子句“翻转”成不同计划类型所需的
形式。例如,考虑一个 WHERE 子句形如 tab1.x = tab2.y
的查询,其中 tab1.x
和 tab2.y 属于用户定义类型,并且假设
tab2.y 上有索引。优化器必须能把该子句翻转为
tab2.y = tab1.x 才能生成索引扫描,因为索引扫描
机制要求看到被索引的列位于它所获得的操作符的左侧。
PostgreSQL 不会简单地
假定这是一次有效的变换 — = 操作符的创建者
必须通过用交换子信息标记该操作符来声明它是有效的。
NEGATOR #
如果提供了 NEGATOR 子句,它会指定一个操作符作为
被定义操作符的取反子。如果两个操作符都返回布尔结果,且对所有可能的
输入 x、y 都有 (x A y) 等于 NOT (x B y),我们就说操作符 A 是操作符
B 的取反子。
注意 B 也是 A 的取反子。
例如,对大多数数据类型而言,< 和
>= 是一对取反子。
一个操作符永远不能合法地作为它自己的取反子。
与交换子不同,一对一元操作符可以合法地被标记为互为取反子;这意味着 对所有 x 都有 (A x) 等于 NOT (B x)。
操作符的取反子必须与被定义的操作符具有相同的左操作数类型和/或右
操作数类型,因此与 COMMUTATOR 一样,在
NEGATOR 子句中只需给出操作符名。
提供取反子对查询优化器非常有帮助,因为它使形如
NOT (x = y) 的表达式可以化简为
x <> y。这种情况比你想象的更常见,因为
NOT 操作可能会作为其他重排的结果被插入。
RESTRICT #
如果提供了 RESTRICT 子句,它会为该操作符指定一个
限制选择性估算函数。(注意这是一个函数名,而不是操作符名。)
RESTRICT 子句只对返回 boolean 的二元
操作符有意义。限制选择性估算器的思路是:猜测表中有多大比例的行会
满足形如下式的 WHERE 子句条件:
column OP constant
其中使用当前操作符和一个特定的常量值。
这通过让优化器大致了解这种形式的 WHERE 子句会
消除多少行来帮助它。
(您可能会想,如果常量在左边会怎样?嗯,这正是
COMMUTATOR 的用途之一……)
编写新的限制选择性估算函数远远超出了本章的范围,但幸运的是,对于您 自己的许多操作符,通常可以直接使用系统的标准估算器之一。 以下是标准的限制选择性估算器:
eqsel 用于 = |
neqsel 用于 <> |
scalarltsel 用于 < |
scalarlesel 用于 <= |
scalargtsel 用于 > |
scalargesel 用于 >= |
对于选择性非常高或非常低的操作符,即使它们实际上并非相等或不等
操作符,也经常可以借用 eqsel 或
neqsel。例如,近似相等的几何操作符就使用
eqsel,其假设是它们通常只会匹配表中很小一部分
条目。
对于那些能够以某种合理方式转换为数值标量以进行范围比较的数据类型,
可以使用 scalarltsel、scalarlesel、
scalargtsel 和 scalargesel
来进行比较。如果可能,请把您的数据类型添加到
src/backend/utils/adt/selfuncs.c 中
convert_to_scalar() 函数所理解的类型之中。
(最终,这个函数应当被替换为通过 pg_type 系统
目录中的一列来标识的按数据类型的函数;但这尚未实现。)
如果您不这样做,系统仍然可以工作,但优化器的估算不会达到应有的
水平。
另一个有用的内建选择性估算函数是 matchingsel,
只要为输入数据类型收集了标准 MCV 和/或直方图统计信息,它几乎可以
用于任何二元操作符。它的默认估算值被设为 eqsel
所用默认估算值的两倍,因此最适合那些严格程度略低于相等性的比较
操作符。(或者您可以调用底层的
generic_restriction_selectivity
函数,并提供不同的默认估算值。)
src/backend/utils/adt/geo_selfuncs.c 中还有为
几何操作符设计的额外选择性估算函数:areasel、
positionsel 和
contsel。撰写本文时它们还只是占位实现,但您
仍然可以使用它们(或者更好的是,改进它们)。
JOIN #
如果提供了 JOIN 子句,它会为该操作符指定一个
连接选择性估算函数。(注意这是一个函数名,而不是操作符名。)
JOIN 子句只对返回 boolean 的二元
操作符有意义。连接选择性估算器的思路是:猜测一对表中有多大比例的
行会满足形如下式的 WHERE 子句条件:
table1.column1 OP table2.column2
其中使用当前操作符。与 RESTRICT 子句一样,这能
极大地帮助优化器,让它得以判断几种可能的连接顺序中哪一种最省力。
与前文一样,本章不打算解释如何编写连接选择性估算函数,只是建议: 如果适用,请使用标准估算器之一:
eqjoinsel 用于 = |
neqjoinsel 用于 <> |
scalarltjoinsel 用于 < |
scalarlejoinsel 用于 <= |
scalargtjoinsel 用于 > |
scalargejoinsel 用于 >= |
matchingjoinsel 用于通用匹配操作符 |
areajoinsel 用于基于二维面积的比较 |
positionjoinsel 用于基于二维位置的比较 |
contjoinsel 用于基于二维包含关系的比较 |
HASHES #
如果存在 HASHES 子句,它告诉系统:允许对基于该
操作符的连接使用哈希连接方法。HASHES 只对返回
boolean 的二元操作符有意义,并且实践中该操作符
必须表示某种数据类型或数据类型对上的相等关系。
哈希连接所依赖的假设是:连接操作符只会对哈希到同一哈希码的左右值对
返回真。如果两个值被放入不同的哈希桶,连接将根本不会比较它们,隐含
地假定连接操作符的结果必然为假。因此,为不表示某种相等关系形式的
操作符指定 HASHES 是毫无意义的。在大多数情况下,
只对两侧采用相同数据类型的操作符支持哈希才是实际可行的。
不过,有时也可以为两种或多种数据类型设计兼容的哈希函数;即对
“相等”的值生成相同哈希码的函数,即使这些值的表示方式
不同。例如,对不同宽度的整数做哈希时,安排这种性质是相当简单的。
要被标记为 HASHES,连接操作符必须出现在一个
哈希索引操作符族中。创建操作符时不会强制这一点,因为引用的操作符
族显然还不存在。但如果不存在这样的操作符族,在哈希连接中使用该
操作符的尝试将在运行时失败。系统需要该操作符族来为操作符的输入
数据类型查找特定于数据类型的哈希函数。当然,在创建操作符族之前,
您也必须先创建合适的哈希函数。
准备哈希函数时应当小心,因为存在一些与机器相关的因素可能导致它
不能正确工作。
例如,如果您的数据类型是一个结构体,其中可能包含无关紧要的填充位,
您就不能简单地把整个结构体传给 hash_any。
(除非您编写其他操作符和函数来确保未使用的位始终为零,这是推荐的
策略。)
另一个例子是,在符合 IEEE
浮点标准的机器上,负零和正零是不同的值(位模式不同),但它们被
定义为比较相等。
如果浮点值可能包含负零,就需要额外的步骤来确保它生成与正零相同的
哈希值。
可哈希连接的操作符必须有一个交换子(当两个操作数数据类型相同时是 它自身,不同时是相关的相等操作符)出现在同一操作符族中。 如果不满足,使用该操作符时可能会发生规划器错误。此外,对于支持 多种数据类型的哈希操作符族,为每种数据类型组合都提供相等操作符是 一个好主意(但并非严格必需);这样可以实现更好的优化。
可哈希连接的操作符的底层函数必须被标记为 immutable 或 stable。 如果它是 volatile 的,系统将永远不会尝试把该操作符用于哈希连接。
如果可哈希连接的操作符的底层函数被标记为 strict,那么该函数还必须
是完备的:也就是说,对于任何两个非空输入,它都应返回真或假,绝不
返回空值。如果不遵守这条规则,对 IN 操作的哈希
优化可能产生错误的结果。(具体来说,IN 可能在
按标准本应返回空值的地方返回假;或者它可能产生一个错误,抱怨它
没有准备处理空结果。)
MERGES #
如果存在 MERGES 子句,它告诉系统:允许对基于该
操作符的连接使用归并连接方法。MERGES 只对返回
boolean 的二元操作符有意义,并且实践中该操作符
必须表示某种数据类型或数据类型对上的相等关系。
归并连接的思路是先把左右两个表排好序,然后并行地扫描它们。因此,
两种数据类型都必须能够被完全排序,并且连接操作符必须只能对在排序
顺序中处于“同一位置”的值对成功。实践中这意味着连接
操作符的行为必须类似相等。但只要两种不同的数据类型在逻辑上兼容,
仍然可以对它们做归并连接。例如,
smallint 与 integer
的相等操作符就是可归并连接的。
我们只需要能把两种数据类型排成逻辑兼容序列的排序操作符。
要被标记为 MERGES,连接操作符必须作为相等成员
出现在一个 btree 索引操作符族中。
创建操作符时不会强制这一点,因为引用的操作符族显然还不存在。但
只有能找到匹配的操作符族,该操作符才会真正被用于归并连接。
MERGES 标志因此充当给规划器的提示,表明值得寻找
匹配的操作符族。
可归并连接的操作符必须有一个交换子(当两个操作数数据类型相同时是
它自身,不同时是相关的相等操作符)出现在同一操作符族中。
如果不满足,使用该操作符时可能会发生规划器错误。此外,对于支持
多种数据类型的 btree 操作符族,为每种数据类型
组合都提供相等操作符是一个好主意(但并非严格必需);这样可以实现
更好的优化。
可归并连接的操作符的底层函数必须被标记为 immutable 或 stable。 如果它是 volatile 的,系统将永远不会尝试把该操作符用于归并连接。