pgsql.cc 提供对 postgresql.org 官网内容的中文翻译,由 Pigsty 团队维护。
由 Tom Lane 撰写。
PostgreSQL 的操作符定义可以包含若干可选子句,告诉系统关于操作符行为的有用信息。只要合适就应当提供这些子句,因为它们能给使用该操作符的查询的执行带来可观的加速。但如果你提供了它们,就必须确保它们是对的!错误地使用优化子句可能导致后端崩溃、微妙错误的输出或其他糟糕后果。如果不确定,你随时可以省略某个优化子句;唯一的后果是查询可能比所需的更慢。
未来的 PostgreSQL 版本可能会添加更多优化子句。这里描述的是 7.3.21 版本理解的所有子句。
如果给出 COMMUTATOR 子句,它指定一个与正在定义的操 作符互为交换子的操作符。若对所有可能的输入值 x、y,都有 (x A y) 等于 (y B x),则称操作符 A 是操作符 B 的交换子。注意,B 也同样是 A 的交换 子。例如,对某种特定数据类型而言,< 和 > 通常互为交换子,而操作符 + 通 常与其自身可交换。但操作符 - 通常并不与任何操作符可 交换。
被交换操作符的左操作数类型与其交换子的右操作数类型相同,反之亦然。因此,只要给出交换子操作符的名称, PostgreSQL 就足以查找到交换子,这也正是 COMMUTATOR 子句需要提供的全部内容。
定义一个与自身可交换的操作符时,直接定义即可。但定义一对可交换操作符时,情况就稍微复杂一些:第一个要定义的操作符如何引用另一个尚未定义的操作符呢?这个问题有两种解决办法:
一种办法是在你定义的第一个操作符中省略 COMMUTATOR 子句,然后在第二个操作符的定义中给出它。由于 PostgreSQL 知道可交换操作符成对出现,当它看到第二个定义时,会自动回过头来补填第一个定义中缺失的 COMMUTATOR 子句。
另一种更直接的办法是在两个定义中都包含 COMMUTATOR 子句。当 PostgreSQL 处理第一个定义并发现 COMMUTATOR 引用了一个不存在的操作符时,系统会在系统目录中为该操作符创建一个占位条目。这个占位条目只在操作符名称、左右操作数类型和结果类型上拥有有效数据,因为那是 PostgreSQL 此时所能推断出的全部。第一个操作符的目录条目会链接到这个占位条目。之后当你定义第二个操作符时,系统会用第二个定义中的额外信息更新该占位条目。如果你在占位条目被填充之前就尝试使用该占位操作符,只会得到一条错误消息。(注意:这一过程在 6.5 之前的 PostgreSQL 版本中不能可靠工作,但现在它是推荐的做法。)
如果给出 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 操作。
可以使用上面解释的定义交换子对的相同方法,来定义成对的求反器操作符。
如果给出 RESTRICT 子句,它指定该操作符的限制选择率估算函数。(注意,这里是函数名,而不是操作符名。) RESTRICT 子句只对返回 boolean 的二元操作符有意义。限制选择率估算器的作用,是猜测一张表中有多少比例的行会满足如下形式的 WHERE 子句条件:
column OP constant
(即针对当前操作符和某个特定常量值)。这会帮助优化器大致了解这种形式的 WHERE 子句会消掉多少行。(你可能在想:如果常量在左边会怎样?嗯,这正是 COMMUTATOR 的用途之一……)
编写新的限制选择率估算函数远远超出了本章的范围,不过幸运的是,对于你自 己的很多操作符,通常都可以直接使用系统提供的某个标准估算器。标准的限制 选择率估算器如下:
eqsel 用于 = |
neqsel 用于 <> |
scalarltsel 用于 < 或 <= |
scalargtsel 用于 > 或 >= |
这些分类可能看起来有点奇怪,但仔细想想就会发现它们是有道理的。= 通常只会接受表中很小一部分行;<> 通常只会拒绝很小一部分行。< 接受的比例取决于给定常量落在该表列的值范围中的什么位置(恰好,这正是 ANALYZE 收集并提供给选择率估算器的信息)。对于相同的比较常量,<= 接受的比例会比 < 略大,但二者足够接近,不值得区分,尤其是无论如何我们很可能也只能做出粗略猜测。类似的说法也适用于 > 和 >=。
对于选择率非常高或非常低的操作符,即使它们实际上并不是真正的相等或不等 比较,你也常常可以勉强使用 eqsel 或 neqsel。例如,几何类型中的近似相等操作符就使用 eqsel,其依据是它们通常只会匹配表中很小一部分项。
对于那些能够以某种合理方式转换为数值标量、从而可进行范围比较的数据类型,你可以使用 scalarltsel 和 scalargtsel。如果可能的话,请把该数据类型加入 src/backend/utils/adt/selfuncs.c 中函数 convert_to_scalar() 所理解的范围。(最终,这个函数应被通过 pg_type 系统目录某一列标识的、按数据类型划分的函数所取代;但这件事目前还没有发生。)如果不这样做,系统仍然可以工作,但优化器的估算效果就不会像本可达到的那样好。
src/backend/utils/adt/geo_selfuncs.c 中还有为几何操作符设计的额外选择率函数: areasel、positionsel 和 contsel。在写作本文时它们只是存根,但你可能还是想用它们(甚至更好的是,改进它们)。
如果给出 JOIN 子句,它指定该操作符的连接选择率估算函数。(注意,这里是函数名,而不是操作符名。) JOIN 子句只对返回 boolean 的二元操作符有意义。连接选择率估算器的作用,是猜测一对表中有多少比例的行会满足如下形式的 WHERE 子句条件:
table1.column1 OP table2.column2
(即针对当前操作符)。与 RESTRICT 子句一样,这也能极大地帮助优化器判断若干可能的连接顺序中哪一个可能耗时最少。
与前面一样,本章不会尝试解释如何编写连接选择率估算函数,而只是建议你在 适用时使用某个标准估算器:
eqjoinsel 用于 = |
neqjoinsel 用于 <> |
scalarltjoinsel 用于 < 或 <= |
scalargtjoinsel 用于 > 或 >= |
areajoinsel 用于基于二维面积的比较 |
positionjoinsel 用于基于二维位置的比较 |
contjoinsel 用于基于二维包含关系的比较 |
如果给出 HASHES 子句,它告诉系统可以对此操作符上的连接使用哈希连接方法。 HASHES 只对返回 boolean 的二元操作符有意义,而且实践中该操作符最好是某种数据类型上的相等操作。
哈希连接背后的假设是:只有当左值和右值被哈希到同一个哈希码时,连接操作 符才有可能返回 true。如果两个值被放进不同的哈希桶,连接过程就根本不会 去比较它们,这实际上隐含假定连接操作符的结果必定为 false。因此,对于那 些并不表示相等关系的操作符,指定 HASHES 永远没有 意义。
事实上,仅逻辑上的相等也不够;该操作符最好表示纯粹的按位相等,因为哈希函数是按值的内存表示计算的,而不管这些位是什么含义。例如,时间间隔的相等就不是按位相等;时间间隔相等操作符认为两个时间间隔只要时长相同就相等,无论其端点是否一致。这意味着在时间间隔字段上使用 = 的连接,如果实现为哈希连接,得到的结果会与其他实现方式不同,因为本应匹配的很大一部分值对会被哈希到不同的值,永远不会被哈希连接比较。而如果优化器选择使用另一种连接,相等操作符判定相等的所有值对都会被找到。我们不想要这种不一致,因此我们没有把时间间隔的相等操作标记为可哈希。
还有一些与机器相关的情形可能让哈希连接出错。例如,如果你的数据类型是一个可能含有无关填充位的结构,把它的相等操作符标记为 HASHES 是不安全的。(除非也许你把其他操作符写成保证未用的位始终为零。)另一个例子是浮点数据类型用于哈希连接是不安全的。在符合 IEEE 浮点标准的机器上,负零和正零是不同的值(位模式不同),但按定义它们比较相等。因此,如果把浮点数据类型上的相等操作符标记为 HASHES,负零和正零可能不会被哈希连接配对,而任何其他连接过程都会把它们配上。
归根结底:你可能只应对那些(可以)用 memcmp() 实现的相等操作符使用 HASHES。
MERGES (SORT1, SORT2, LTCMP, GTCMP)如果给出 MERGES 子句,它告诉系统可以对此操作符上的连接使用归并连接方法。 MERGES 只对返回 boolean 的二元操作符有意义,而且实践中该操作符必须表示某种数据类型或某对数据类型上的相等。
归并连接基于这样的思想:把左右两边的表排好序,然后并行扫描它们。因此,两个数据类型都必须能够全序化,而且连接操作符必须只在处于排序次序中“相同位置”的值对上才可能成功。实践中这意味着连接操作符的行为必须像相等一样。但与哈希连接不同——哈希连接要求左右数据类型最好相同(或至少按位等价)——只要两个不同的数据类型在逻辑上兼容,就可以对它们做归并连接。例如, int2 对 int4 的相等操作符就是可归并连接的。我们只需要能把两个数据类型排成逻辑兼容次序的排序操作符。
归并连接的执行要求系统能够识别出与该可归并连接相等操作符相关的四个操作符:左输入数据类型的小于比较、右输入数据类型的小于比较、两种数据类型之间的小于比较,以及两种数据类型之间的大于比较。(如果可归并连接的操作符具有两个不同的输入数据类型,这实际上是四个不同的操作符;但当两个输入类型相同时,三个小于操作符是同一个操作符。)可以分别通过名称指定这些操作符,即分别使用 SORT1、 SORT2、 LTCMP 和 GTCMP 选项。在指定 MERGES 时,如果省略了其中任何一个,系统将分别填入默认名称 <、<、<、>。 此外,如果这四个操作符选项中任何一个出现,就会认为隐含了 MERGES,因此可以只指定其中一部分,而让系统补全其余部分。
这四个比较操作符的输入数据类型可以从可归并连接操作符的输入类型推断出来,因此与 COMMUTATOR 一样,这些子句中只需给出操作符名。除非你选用了一些不同寻常的操作符名称,否则只写 MERGES 并让系统补全细节就足够了。(与 COMMUTATOR 和 NEGATOR 一样,如果你碰巧先定义了相等操作符、后定义其他操作符,系统也能够创建占位操作符条目。)
对于你标记为可归并连接的操作符,还有一些附加限制。这些限制目前不会被 CREATE OPERATOR 检查,但如果其中任何一条不成立,在 使用该操作符时可能会出错:
一个可归并连接的相等操作符必须拥有一个可归并连接的交换子(如果两个 数据类型相同,则为它自身;如果不同,则应是相关的相等操作符)。
如果存在一个可归并连接的操作符关联某两个数据类型 A 和 B,又存在另一 个可归并连接的操作符关联 B 和任何第三种数据类型 C,那么 A 和 C 也必须 有一个可归并连接的操作符;换句话说,可归并连接操作符的存在必须是可 传递的。
如果你指定的四个比较操作符没有以兼容的方式对数据值排序,运行时会出 现奇怪的结果。
在 7.3 之前的 PostgreSQL 版本中,没有 MERGES 简写:要创建可归并连接的操作符,必须显式写出 SORT1 和 SORT2 两者。此外, LTCMP 和 GTCMP 选项也不存在;这些操作符的名称被硬编码为 < 和 >。
译文有误、术语不当或页面显示问题,请到译文仓库 pgsty/pgdoc 报告译文问题。 英文原文本身的问题,请在当前版本的对应页面向上游反馈;上游不再修订已结束维护的版本。