受支持版本: 当前版本 (18) / 17 / 16 / 15 / 14
开发版本: devel

36.15. 操作符优化信息 #

PostgreSQL 的操作符定义可以包含若干可选子句, 用来告诉系统该操作符行为的一些有用信息。只要适用,就应当提供这些子句, 因为它们可以显著加快使用该操作符的查询执行速度。但如果你提供了这些信息, 就必须确保它们是正确的!错误地使用优化子句可能导致查询变慢、输出出现隐 蔽错误,或者引发其他糟糕后果。如果你拿不准,完全可以省略某个优化子句; 唯一的后果,只是查询可能会比本来需要的更慢。

未来版本的 PostgreSQL 可能会加入更多优化子 句。这里描述的是 16.13 版本所理解的全部子句。

也可以把规划器支持函数附加到某个作为操作符底层实现的函数上,这也提供了 一种向系统说明该操作符行为的方式。更多信息见 Section 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.xtab2.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 用于 >=

对于选择度非常高或非常低的操作符,即使它们实际上并不是真正的相等或不等 比较,你也常常可以勉强使用 eqselneqsel。例如,几何类型中的近似相等操作符就使用 eqsel,其依据是它们通常只会匹配表中很小一部分项。

对于那些能够以某种合理方式转换为数值标量、从而可进行范围比较的数据类 型,你可以使用 scalarltselscalarleselscalargtselscalargesel。如果可能的话,请把该数据类型加入函数 convert_to_scalar()(位于 src/backend/utils/adt/selfuncs.c)所理解的范围 中。(最终,这个函数应被通过 pg_type 系统目录某一 列标识的、按数据类型划分的函数所取代;但这件事目前还没有发生。)如果不 这样做,系统仍然可以工作,但优化器的估算效果就不会像本可达到的那样好。

另一个有用的内置选择度估算函数是 matchingsel。只要 已为输入数据类型收集了标准的 MCV 和/或直方图统计信息,它几乎就适用于任 何二元操作符。它的默认估计值被设定为 eqsel 所用默认 估计值的两倍,因此最适合那些比相等比较稍微宽松一些的比较操作符。(或者 你也可以调用底层的 generic_restriction_selectivity 函数,并提供不同的默认估计值。)

src/backend/utils/adt/geo_selfuncs.c 中,还为几 何操作符提供了其他选择度估算函数:areaselpositionselcontsel。截至 目前,这些函数都还只是桩实现,但你也许仍会想使用它们(或者更好的是,改 进它们)。

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 的二元操作符有意义;在实践中,该操作符还必须 表示某种数据类型或某对数据类型上的相等关系。

哈希连接背后的假设是:只有当左值和右值被哈希到同一个哈希码时,连接操作 符才有可能返回 true。如果两个值被放进不同的哈希桶,连接过程就根本不会 去比较它们,这实际上隐含假定连接操作符的结果必定为 false。因此,对于那 些并不表示某种相等关系的操作符,指定 HASHES 永远没有 意义。在大多数情况下,只有对两侧都接受同一数据类型的操作符支持哈希才是 现实可行的。不过,有时也可以为两种或更多数据类型设计兼容的哈希函数;也 就是说,尽管这些值的表示不同,但对于相等的值,函数仍会 生成相同的哈希码。例如,对不同位宽的整数做到这一点就相当简单。

若要被标记为 HASHES,该连接操作符必须出现在某个哈希 索引操作符族中。创建操作符时并不会强制这一点,因为那时引用它的操作符族 当然还不存在。但如果没有这样的操作符族存在,运行时尝试在哈希连接中使用 该操作符就会失败。系统需要通过该操作符族来查找与该操作符输入数据类型相 对应的哈希函数。当然,在创建该操作符族之前,你还必须先创建合适的哈希函 数。

准备哈希函数时应格外小心,因为它有一些依赖机器的方式可能会导致结果不正 确。例如,如果你的数据类型是某种结构体,其中可能包含无意义的填充位,那 就不能简单地把整个结构传给 hash_any。(除非你编写 了其他操作符和函数,以确保这些未使用位始终为零,而这正是推荐的策略。) 再比如,在符合 IEEE 浮点标准的机器上,负零和正零是 不同的值(位模式不同),但它们被定义为比较相等。如果某个浮点值可能包含 负零,就需要采取额外步骤,确保它生成与正零相同的哈希值。

一个可参与哈希连接的操作符,必须拥有一个交换子(如果两侧操作数数据类型 相同,则可以是它自身;如果不同,则应是相关的相等操作符),并且该交换子 也必须出现在同一个操作符族中。否则,在使用该操作符时可能会发生规划器错 误。另外,对于支持多种数据类型的哈希操作符族,最好(虽然并非严格必需) 为每一种数据类型组合都提供相等操作符;这样可以获得更好的优化效果。

Note

作为可哈希连接操作符底层实现的函数,必须标记为 immutable 或 stable。 如果它是 volatile,系统就永远不会尝试把该操作符用于哈希连接。

Note

如果某个可哈希连接操作符的底层函数被标记为 strict,那么该函数还必须是 完备的:也就是说,对于任意两个非空输入,它都应返回 true 或 false,而绝 不能返回 null。如果不遵循这条规则,对 IN 操作进行哈 希优化时可能会产生错误结果。(具体来说,IN 可能会在 按标准本应返回 null 的地方返回 false;或者抛出一条错误,抱怨它没有为 null 结果做好准备。)

36.15.6. MERGES #

如果存在 MERGES 子句,它会告知系统:在基于该操作符 的连接中,可以使用归并连接方法。MERGES 只对返回 boolean 的二元操作符有意义;在实践中,该操作符还必须 表示某种数据类型或某对数据类型上的相等关系。

归并连接的基本思想,是先把左表和右表分别排序,然后并行扫描它们。因此, 两种数据类型都必须能够被完全排序,而连接操作符必须只能在那对值位于排序 次序中同一位置时才成功。实际效果上,这意味着连接操作符 必须表现得像相等比较一样。不过,只要两种不同的数据类型在逻辑上兼容,也 完全可以对它们进行归并连接。例如,smallintinteger 之间的相等操作符就是可归并连接的。我们只需要能把 两种数据类型都带入逻辑兼容排序序列的排序操作符即可。

若要被标记为 MERGES,该连接操作符必须作为相等成员出 现在某个 btree 索引操作符族中。创建操作符时并不会强 制这一点,因为那时引用它的操作符族当然还不存在。但除非能找到匹配的操作 符族,否则该操作符实际上不会被用于归并连接。因此, MERGES 标记的作用,是向规划器提供一个提示,告诉它值 得去查找匹配的操作符族。

一个可参与归并连接的操作符,必须拥有一个交换子(如果两侧操作数数据类型 相同,则可以是它自身;如果不同,则应是相关的相等操作符),并且该交换子 也必须出现在同一个操作符族中。否则,在使用该操作符时可能会发生规划器错 误。另外,对于支持多种数据类型的 btree 操作符族,最 好(虽然并非严格必需)为每一种数据类型组合都提供相等操作符;这样可以获 得更好的优化效果。

Note

作为可归并连接操作符底层实现的函数,必须标记为 immutable 或 stable。如 果它是 volatile,系统就永远不会尝试把该操作符用于归并连接。

提交更正

如果您发现文档中有不正确的内容、与您使用特定功能的经验不符或需要进一步说明,请使用此表单来报告文档问题。