2024 年 9 月 26 日: PostgreSQL 17 发布!
支持版本:当前 (17) / 16 / 15 / 14 / 13 / 12
开发版本:devel
不支持版本:11 / 10 / 9.6 / 9.5 / 9.4 / 9.3 / 9.2 / 9.1 / 9.0 / 8.4 / 8.3 / 8.2 / 8.1 / 8.0 / 7.4 / 7.3 / 7.2

36.15. 运算符优化信息 #

一个 PostgreSQL 运算符定义可以包含一些可选的子句,这些子句告诉系统有关运算符行为的有用信息。这些子句应该在适当的时候提供,因为它们可以显著提高使用该运算符的查询的执行速度。但是,如果你提供它们,你必须确保它们是正确的!不正确使用优化子句会导致查询变慢、输出细微错误或其他不良后果。如果你不确定,可以始终省略优化子句;唯一的后果是查询可能运行得比需要的时间更长。

在未来的 PostgreSQL 版本中可能会添加其他优化子句。这里描述的子句是 17.0 版本理解的所有子句。

还可以将一个计划器支持函数附加到运算符底层的函数,提供另一种方法来告诉系统有关运算符行为的信息。有关更多信息,请参阅 第 36.11 节

36.15.1. COMMUTATOR #

如果提供 COMMUTATOR 子句,它将命名一个作为被定义运算符的交换子的运算符。我们说运算符 A 是运算符 B 的交换子,如果 (x A y) 等于 (y B x),对于所有可能的输入值 x, y。请注意,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 A y) 等于 NOT (x B y),对于所有可能的输入 x, y。请注意,B 也是 A 的否定运算符。例如,对于大多数数据类型,<>= 是一对否定运算符。运算符永远不能有效地是它自身的否定运算符。

与交换子不同,一对一元运算符可以有效地被标记为彼此的否定运算符;这意味着 (A x) 等于 NOT (B x),对于所有 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。如果可能,将数据类型添加到 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。在撰写本文时,这些只是存根,但你可能仍然想使用它们(或者更好的是,改进它们)。

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 的二元运算符有意义,实际上该运算符必须表示某些数据类型或数据类型对的相等性。

散列连接的假设是,连接运算符仅对散列到相同散列代码的左右值的配对返回真。如果两个值被放入不同的散列桶中,连接将根本不会比较它们,隐式假设连接运算符的结果必须为假。因此,对于不表示某些形式的相等性的运算符来说,指定 HASHES 没有意义。在大多数情况下,仅对两边采用相同数据类型的运算符支持散列才切实可行。但是,有时可以为两种或多种数据类型设计兼容的散列函数;也就是说,即使值具有不同的表示形式,也能够为相等值生成相同的散列代码的函数。例如,当对不同宽度的整数进行散列时,安排此属性相当简单。

为了被标记为 HASHES,连接运算符必须出现在散列索引运算符族中。当您创建运算符时不会强制执行此操作,因为当然引用运算符族还不存在。但是,如果不存在这样的运算符族,尝试在散列连接中使用该运算符将在运行时失败。系统需要运算符族来查找运算符输入数据类型的特定于数据类型的散列函数。当然,您还必须在创建运算符族之前创建合适的散列函数。

在准备散列函数时应谨慎,因为存在可能使其无法正常执行的依赖于机器的方式。例如,如果您的数据类型是结构体,其中可能存在不重要的填充位,则不能简单地将整个结构体传递给 hash_any。(除非您编写其他运算符和函数来确保未使用的位始终为零,这是推荐的策略。)另一个例子是,在满足以下条件的机器上IEEE浮点标准,负零和正零是不同的值(不同的位模式),但它们被定义为相等。如果浮点值可能包含负零,则需要额外的步骤来确保它生成与正零相同的散列值。

可散列连接的运算符必须有一个交换子(如果两个操作数数据类型相同,则为自身;如果它们不同,则为相关的相等运算符),该交换子出现在同一个运算符族中。如果不是这种情况,在使用运算符时可能会发生规划器错误。此外,对于支持多种数据类型的散列运算符族,最好(但不是严格要求)为每种数据类型的组合提供相等运算符;这可以实现更好的优化。

注意

可散列连接运算符的底层函数必须标记为不可变或稳定。如果它易变,系统将永远不会尝试将运算符用于散列连接。

注意

如果可散列连接运算符有一个标记为严格的底层函数,则该函数也必须完整:也就是说,对于任何两个非空输入,它应该返回真或假,而不是空值。如果不遵循此规则,IN 操作的散列优化可能会生成错误的结果。(具体来说,IN 可能在标准认为正确答案为空值的情况下返回假;或者它可能产生一个错误,抱怨它没有为空值结果做好准备。)

36.15.6. MERGES #

如果存在,MERGES 子句告诉系统,允许使用合并连接方法来执行基于此运算符的连接。 MERGES 仅对返回 boolean 的二元运算符有意义,实际上该运算符必须表示某些数据类型或数据类型对的相等性。

合并连接基于对左表和右表进行排序,然后并行扫描它们的想法。因此,两种数据类型都必须能够被完全排序,并且连接运算符必须是仅对落在排序顺序中相同位置的值配对才能成功的运算符。实际上,这意味着连接运算符必须表现得像相等性一样。但只要两种不同的数据类型在逻辑上兼容,就可以合并连接它们。例如,smallintinteger 的相等运算符是可合并连接的。我们只需要排序运算符,将两种数据类型都带入逻辑上兼容的序列中。

为了被标记为 MERGES,连接运算符必须作为 btree 索引运算符族中的相等成员出现。当您创建运算符时不会强制执行此操作,因为当然引用运算符族还不存在。但是,除非找到匹配的运算符族,否则该运算符实际上不会用于合并连接。因此,MERGES 标志充当对规划器的提示,表明值得寻找匹配的运算符族。

可合并连接的运算符必须有一个交换子(如果两个操作数数据类型相同,则为自身;如果它们不同,则为相关的相等运算符),该交换子出现在同一个运算符族中。如果不是这种情况,在使用运算符时可能会发生规划器错误。此外,对于支持多种数据类型的 btree 运算符族,最好(但不是严格要求)为每种数据类型的组合提供相等运算符;这可以实现更好的优化。

注意

可合并连接运算符的底层函数必须标记为不可变或稳定。如果它易变,系统将永远不会尝试将运算符用于合并连接。

提交更正

如果您在文档中发现任何不正确的内容、与您对特定功能的体验不符或需要进一步澄清的内容,请使用 此表格 报告文档问题。