2025年9月25日: PostgreSQL 18 发布!
支持的版本: 当前 (18) / 17 / 16 / 15 / 14 / 13
开发版本: devel
不支持的版本: 12 / 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 可能会添加额外的优化子句。这里描述的子句是版本 18.0 理解的所有子句。

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

36.15.1. COMMUTATOR #

如果提供了 COMMUTATOR 子句,则它指定一个运算符,该运算符是被定义运算符的交换算子。我们说运算符 A 是运算符 B 的交换算子,如果对于所有可能的输入值 x, y,都有 (x A y) 等于 (y B x)。请注意,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)。请注意,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

(您可能会想,如果常量在左边怎么办?嗯,这就是 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 用于基于 2D 区域的比较
positionjoinsel 用于基于 2D 位置的比较
contjoinsel 用于基于 2D 包含的比较

36.15.5. HASHES #

如果存在 HASHES 子句,则它告知系统,可以使用哈希连接方法来基于此运算符进行连接。HASHES 仅对返回 boolean 的二元运算符有意义,并且实际上该运算符必须代表某种数据类型或数据类型对的相等性。

哈希连接的根本假设是,连接运算符只能对哈希到相同哈希码的左值和右值对返回 true。如果两个值被放入不同的哈希桶中,连接将永远不会比较它们,这隐含地假定连接运算符的结果必须是 false。因此,为不代表某种形式的相等性的运算符指定 HASHES 没有任何意义。在大多数情况下,仅为两侧采用相同数据类型的运算符支持哈希是可行的。然而,有时可以设计兼容的哈希函数来处理两种或多种数据类型;也就是说,即使值具有不同的表示形式,这些函数也能为“相等”的值生成相同的哈希码。例如,当哈希不同宽度整数时,很容易实现此属性。

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

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

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

注意

可哈希连接的运算符的底层函数必须被标记为 immutable 或 stable。如果它是易变的,系统将永远不会尝试将该运算符用于哈希连接。

注意

如果可哈希连接的运算符具有一个标记为 strict 的底层函数,那么该函数还必须是 complete:也就是说,它应该为任何两个非空输入返回 true 或 false,绝不返回 null。如果不遵守此规则,则哈希优化 IN 操作可能会产生错误的结果。(具体来说,IN 可能会返回 false,而根据标准,正确答案应为 null;或者它可能会产生一个错误,抱怨它没有准备好处理 null 结果。)

36.15.6. MERGES #

如果存在 MERGES 子句,则它告知系统,可以使用合并连接方法来基于此运算符进行连接。MERGES 仅对返回 boolean 的二元运算符有意义,并且实际上该运算符必须代表某种数据类型或数据类型对的相等性。

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

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

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

注意

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

提交更正

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