38.15. 运算符优化信息#
PostgreSQL运算符定义可以包含几个可选子句,这些子句向系统传达有关运算符行为的有用信息。应在适当的时候提供这些子句,因为它们可以极大地提高使用该运算符的查询的执行速度。但是,如果您提供它们,您必须确保它们是正确的!不正确地使用优化子句会导致查询速度变慢、输出微妙错误或其他不良后果。如果您不确定,您始终可以省略优化子句;唯一的后果是查询可能比需要的时间更长。
在未来版本的PostgreSQL中可能会添加其他优化子句。此处描述的子句是 16.2 版本理解的所有子句。
还可以将规划器支持函数附加到运算符底层函数,这提供了另一种方式来告知系统运算符的行为。有关详细信息,请参见第 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.x
和tab2.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)。请注意,B 也是 A 的否定符。例如,对于大多数数据类型,<
和>=
是一个否定符对。一个操作符永远不能有效地成为其自身的否定符。
与交换符不同,一对一元操作符可以有效地标记为彼此的否定符;这意味着对于所有 x,(A x) 等于 NOT (B x)。
操作符的否定符必须具有与要定义的操作符相同的左和/或右操作数类型,因此,就像使用COMMUTATOR
一样,只需在NEGATOR
子句中给出操作符名称即可。
提供否定符对查询优化器非常有帮助,因为它允许将诸如NOT (x = y)
之类的表达式简化为x <> y
。这比您想象的出现得更频繁,因为NOT
操作可以作为其他重新排列的结果插入。
可以使用上面针对交换符对解释的相同方法定义否定符操作符对。
38.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
,这些数据类型有一些明智的方法可以转换为用于范围比较的数字标量。如果可能,将数据类型添加到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
。在撰写本文时,这些只是存根,但您可能无论如何都想使用它们(甚至改进它们)。
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
的二元运算符有意义,并且在实践中,该运算符必须表示某些数据类型或数据类型对的相等性。
哈希联接背后的假设是联接运算符只能对哈希到相同哈希代码的左右值对返回 true。如果将两个值放入不同的哈希存储桶,则联接根本不会比较它们,隐式地假设联接运算符的结果必须为 false。因此,对不表示某种形式的相等性的运算符指定HASHES
永远没有意义。在大多数情况下,仅支持对两侧采用相同数据类型的运算符进行哈希处理才切实可行。但是,有时可以为两种或更多数据类型设计兼容的哈希函数;也就是说,即使值具有不同的表示形式,也会为““相等””值生成相同的哈希代码。例如,在对不同宽度的整数进行哈希处理时,安排此属性相当简单。
要标记为HASHES
,联接运算符必须出现在哈希索引运算符系列中。在创建运算符时不会强制执行此操作,因为当然引用的运算符系列还不可能存在。但是,如果不存在这样的运算符系列,则在哈希联接中使用运算符的尝试将在运行时失败。系统需要运算符系列来查找运算符输入数据类型的特定于数据类型哈希函数。当然,在创建运算符系列之前,您还必须创建合适的哈希函数。
准备哈希函数时应小心谨慎,因为有可能会以机器相关的方式导致哈希函数无法执行正确操作。例如,如果数据类型是可能包含无意义填充位的结构,则不能简单地将整个结构传递给hash_any
。(除非编写其他运算符和函数以确保始终将未使用位设为零,这是推荐的策略。)另一个示例是在符合IEEE浮点标准的机器上,负零和正零是不同的值(不同的位模式),但定义为相等比较。如果浮点值可能包含负零,则需要采取额外的步骤来确保它生成与正零相同哈希值。
可哈希连接的运算符必须具有换向器(如果两个操作数数据类型相同,则为自身;如果不同,则为相关的相等运算符),该换向器出现在同一运算符系列中。如果不是这种情况,则在使用运算符时可能会出现规划器错误。此外,对于支持多种数据类型的哈希运算符系列,为每种数据类型的组合提供相等运算符是一个好主意(但不是严格要求);这允许更好的优化。
注意
可哈希连接运算符的底层函数必须标记为不可变或稳定。如果它易变,则系统永远不会尝试将运算符用于哈希连接。
注意
如果可哈希连接运算符具有标记为严格的底层函数,则该函数还必须是完整的:也就是说,对于任何两个非空输入,它应返回 true 或 false,绝不返回 null。如果不遵循此规则,则IN
操作的哈希优化可能会生成错误的结果。(具体来说,IN
可能会返回 false,而根据标准的正确答案应为 null;或者它可能会产生错误,抱怨它没有为 null 结果做好准备。)
38.15.6.MERGES
#
如果存在MERGES
子句,则它会告诉系统允许对基于此运算符的连接使用合并连接方法。对于返回boolean
的二元运算符,MERGES
才具有意义,并且在实践中,该运算符必须表示某种数据类型或数据类型对的相等性。
合并连接基于按顺序对左右表进行排序,然后并行扫描它们的思想。因此,两种数据类型都必须能够完全排序,并且连接运算符必须是仅对按排序顺序位于“相同位置”的值对才能成功的运算符。在实践中,这意味着连接运算符必须表现得像相等性。但是,只要在逻辑上兼容,就可以合并连接两种不同的数据类型。例如,smallint
与integer
相等运算符是可合并连接的。我们只需要将两种数据类型都带入逻辑兼容序列的排序运算符。
要标记为MERGES
,连接运算符必须作为btree
索引运算符系列的相等成员出现。在创建运算符时不会强制执行此操作,因为当然引用的运算符系列还不能存在。但是,除非可以找到匹配的运算符系列,否则实际上不会将运算符用于合并连接。MERGES
标志因此充当了规划器的提示,表明值得寻找匹配的运算符系列。
可合并连接的运算符必须具有换向器(如果两个操作数数据类型相同,则为它本身,如果它们不同,则为相关的相等运算符),该换向器出现在相同的运算符系列中。如果不是这种情况,则在使用运算符时可能会发生规划器错误。此外,对于支持多种数据类型的btree
运算符系列,为每种数据类型的组合提供相等运算符是一个好主意(但不是严格要求的);这允许更好的优化。
注意
可合并连接运算符的底层函数必须标记为不可变或稳定。如果它易变,系统将永远不会尝试将运算符用于合并连接。