38.15. 操作符优化信息

38.15.1. COMMUTATOR
38.15.2. NEGATOR
38.15.3. RESTRICT
38.15.4. JOIN
38.15.5. HASHES
38.15.6. MERGES

一个PostgreSQL的操作符定义能够包括几种可选的子句,它们可以把有关操作符行为的有用的事情告诉系统。只要合适就应该提供这些子句,因为它们能够为使用该操作符的查询带来可观的速度提升。但是如果你提供了它们,你必须确保它们是正确的!不正确地使用一个优化子句可能导致很慢的查询、错误的输出或者其他不好的事情。如果你没有把握你可以总是省去优化子句,这样做的唯一后果是查询会比正常的速度慢。

PostgreSQL的未来版本中可能会增加更多的优化子句。这里描述的优化子句都是版本 14.1 能理解的。

还可以将计划器支持的函数附加到作为操作符基础的函数中,从而提供另一种向系统讲述操作符行为的方法。更多信息参见第 38.11 节

38.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无法简单地假定有一个可用的变换 — =操作符的创建者必须指定它是合法的(通过为该操作符标记交换子信息)。

在你定义一个子交换的操作符时,你这样做就行了。自拟定义一堆交换的操作符时,事情有一点棘手:如何在没有定义第二个操作符时完成第一个操作符的定义?因为第一个操作符需要第二个操作符作为其交换子。对这个问题有两种解决方案:

  • 一种方法是忽略你定义的第一个操作符的COMMUTATOR子句,并且然后在第二个操作符的定义中提供第一个操作符作为交换子。由于PostgreSQL知道交换的操作符是成对出现的,当它看到第二个定义时它将自动回去并且填上第一个定义中缺失的COMMUTATOR子句。

  • 另一种更直接的方法是就在两个定义中包括COMMUTATOR子句。当PostgreSQL处理第一个定义并且意识到COMMUTATOR引用了一个不存在的操作符时,系统将为那个操作符在系统目录中创造一个虚拟项。这个虚拟项只有操作符名称、左右操作数类型和结果类型的数据,因为这些是PostgreSQL在此时能够推断出来的所有东西。第一个操作符的目录项将会链接到这个虚拟项。稍后,当你定义第二个操作符时,系统用来自第二个定义的额外信息更新那个虚拟项。如果你尝试在虚拟操作符还未被填充之前使用它,你将只会得到一个错误消息。

38.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操作可能会被作为其他调整的结果被插入。

求反器对的定义可以使用与定义交换子对相同的方法来完成。

38.15.3. RESTRICT

如果提供了RESTRICT子句,它为该操作符指定一个限制选择度估计函数(注意这是一个函数名而不是一个操作符名)。RESTRICT子句只对返回boolean的二元操作符有意义。一个限制选择度估计器背后的思想是猜测一个表中有多大比例的行对于当前的操作符和一个特定的常数值将会满足一个

column OP constant

形式的WHERE子句条件。这能通过告知优化器具有这种形式的WHERE子句将会消除掉多少行来协助它的工作(你可能会好奇,如果常数位于左部会发生什么?好吧,COMMUTATOR就是干这个的)。

编写一个新的限制选择度估算函数已经超出了本章的范围,但是幸运地是你通常可以将系统的一个标准估算器用于很多你自己的操作符。标准的限制估算器有:

eqsel用于=
neqsel用于<>
scalarltsel用于<
scalarlesel用于<=
scalargtsel用于>
scalargesel用于>=

你能经常成功地为具有非常高或者非常低选择度的操作符使用eqselneqsel,即使它们实际上并非相等或不相等。例如,近似相等几何操作符使用eqsel的前提是假定它们通常只匹配表中的一小部分项。

你可以使用scalarltselscalarleselscalargtsel以及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中的几何操作符设计的:areaselpositionselcontsel。在写这份材料时,这些还只是存根,但是你可能想要使用它们(或者甚至改进它们)。

38.15.4. JOIN

如果提供了JOIN子句,表示用于该操作符的一个连接选择度估计函数(注意这是一个函数名而不是一个操作符名)。JOIN子句只对返回boolean的二元操作符有意义。一个连接选择度估算器背后的思想是猜测一对表中有多大比例的行对于当前的操作符将会满足一个

table1.column1 OP table2.column2

形式的WHERE子句条件。和RESTRICT子句一样,这通过让优化器知道哪种连接序列需要做的工作最少来极大地帮助优化器。

一如既往,这一章将不会尝试解释如何编写一个连接选择度估算函数,而只是建议你在适当的时候使用一种标准估算器:

eqjoinsel用于=
neqjoinsel用于<>
scalarltjoinsel用于<
scalarlejoinsel用于<=
scalargtjoinsel用于>
scalargejoinsel用于>=
matchingjoinsel用于通用匹配运算符
areajoinsel用于基于 2D 区域比较
positionjoinsel用于基于 2D 位置比较
contjoinsel用于基于 2D 包含比较

38.15.5. HASHES

如果存在HASHES子句,它告诉系统它被许可为基于这个操作符的一个连接使用哈希连接方法。HASHES只对返回boolean的二元操作符有意义,并且实际上该操作符必须必须表达某种数据类型或数据类型对的相等。

哈希连接之下的假设是连接操作符只能对哈希到相同哈希码的左右值返回真。如果两个值被放到不同的哈希桶中,连接将根本不会比较它们,这隐式地假定该连接操作符的结果必须是假。因此,为不表示某种形式相等的操作符指定HASHES是没有意义的。在大部分情况下,只有为在两端都是相同数据类型的操作符支持哈希才有意义。不过,有时可以为两种或更多数据类型设计兼容的哈希函数,也就是说,对于相等的值(即使具有不同的表达)会产生相同哈希码的函数。例如,在哈希不同宽度的证书时,安排这个属性相当简单。

要被标记为HASHES,连接操作符必须出现在一个哈希索引操作符族中。当你创建该操作符时这不会被强制,因为要引用的操作符族当然不可能已经存在。但是如果这样的操作符族不存在,尝试在哈希连接中使用该操作符将在运行时失败。系统需要用该操作符族来为操作符的输入数据类型寻找数据类型相关的哈希函数。当然,在创建操作符族之前,你还必须创建合适的哈希函数。

在准备一个哈希函数时应当慎重,因为有一些方法是依赖于机器的,这样它可能无法做正确的事情。例如,如果你的数据类型是一个结构,其中可能有无用的填充位,你不能简单地把整个结构传递给hash_any(除非你编写你自己的操作符和函数按照推荐的策略来保证未被使用的位总是为零)。另一个例子是在符合IEEE浮点标准的机器上,负数零和正数零是不同的值(不同的位模式),但是它们被定义为相等。如果一个浮点值可能包含负数零,那么需要额外的步骤来保证它产生的哈希值与正数零产生的相同。

一个可哈希连接的操作符必须拥有一个出现在同一操作符族中的交换子(如果两个操作数数据类型相同,那么就是它自身,否则是一个相关的相等操作符)。如果情况不是这样,在使用该操作符时,可能会发生规划器错误。此外,一个支持多种数据类型的哈希操作符族为数据类型的每一种组合都提供相等操作符是一个好主意(但是并不被严格要求),这会带来更好的优化。

注意

一个可哈希连接的操作符底层的函数必须被标记为可交换或者稳定。如果它是不稳定的,系统将永远不会为一个哈希连接尝试使用该操作符。

注意

如果一个可哈希连接的操作符有一个被标记为 strict 的底层函数,该函数必须也是 complete:也就是对于任意两个非空输入它应当返回真或假,从不会返回空。如果没有遵守这个规则,IN操作的哈希优化可能产生错误的结果(特别是,当依据标准的正确答案可能是空时,IN可能会返回假,或者它会产生一个错误来抱怨它没有准备会收到一个空结果)。

38.15.6. MERGES

如果存在MERGES子句,它告诉系统它被许可为基于这个操作符的一个连接使用归并连接方法。MERGES只对返回boolean的二元操作符有意义,并且实际上该操作符必须必须表达某种数据类型或数据类型对的相等。

归并连接的思想是排序左右手表并且接着并行扫描它们。这样,两种数据类型必须能够被完全排序,并且该连接操作符必须只为落在排序顺序上同一位置的值对返回成功。实际上这意味着该连接操作符必须和相等的行为一样。但是只要两种不同的数据类型在逻辑上是兼容的,就能对它们使用归并连接。例如,smallint-versus-integer相等操作符就是可归并连接的。我们只需要将两种数据类型变成逻辑上兼容的序列的排序操作符。

要被标记为MERGES,该连接操作符必须作为一个btree索引操作符的相等成员出现。当你创建该操作符时,这不是强制的,因为要引用的操作符族当然可能还不存在。但是除非能找到一个匹配的操作符族,否则该操作符将不会被实际用于归并连接。MERGES标志因此扮演着一种对于规划器的提示,表示值得去寻找一个匹配的操作符族。

一个可归并连接的操作符必须拥有一个出现在同一操作符族中的交换子(如果两个操作数数据类型相同,那么就是它自身,否则是一个相关的相等操作符)。如果情况不是这样,在使用该操作符时,可能会发生规划器错误。此外,一个支持多种数据类型的btree操作符族为数据类型的每一种组合都提供相等操作符是一个好主意(但是并不被严格要求),这会带来更好的优化。

注意

一个可归并连接的操作符底层的函数必须被标记为可交换或者稳定。如果它是不稳定的,系统将永远不会为一个归并连接尝试使用该操作符。