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