pgsql.cc 提供对 postgresql.org 官网内容的中文翻译,由 Pigsty 团队维护。
目录
Postgres 支持左一元、右一元和二元操作符。操作符可以重载;也就是说,同一个操作符名可用于参数数量和类型不同的多个操作符。如果出现歧义情况而系统无法确定该用哪个操作符,它会返回一个错误。你可能需要对左操作数和/或右操作数进行类型转换,帮助系统理解你想用哪个操作符。
每个操作符都是对某个完成实际工作的底层函数调用的"语法糖";因此你必须先创建底层函数,然后才能创建操作符。然而,操作符不仅仅是语法糖,因为它还携带额外信息,帮助查询规划器优化使用该操作符的查询。本章的大部分篇幅将用于解释这些额外信息。
下面是创建一个把两个复数相加的操作符的例子。我们假设已经创建过 complex 类型的定义。首先需要一个完成工作的函数,然后就可以定义操作符:
CREATE FUNCTION complex_add(complex, complex)
RETURNS complex
AS '$PWD/obj/complex.so'
LANGUAGE 'c';
CREATE OPERATOR + (
leftarg = complex,
rightarg = complex,
procedure = complex_add,
commutator = +
);
现在我们可以这样做:
SELECT (a + b) AS c FROM test_complex; +----------------+ |c | +----------------+ |(5.2,6.05) | +----------------+ |(133.42,144.95) | +----------------+
我们在这里展示了如何创建二元操作符。要创建一元操作符,只需省略 leftarg(左一元)或 rightarg(右一元)之一即可。procedure 子句和参数子句是 CREATE OPERATOR 中仅有的必选项。例子中展示的 COMMUTATOR 子句是给查询优化器的一个可选提示。关于 COMMUTATOR 和其他优化器提示的更多细节见下文。
由 Tom Lane 撰写。
Postgres 的操作符定义可以包含若干可选子句,告诉系统关于操作符行为的有用信息。只要合适就应当提供这些子句,因为它们能给使用该操作符的查询的执行带来可观的加速。但如果你提供了它们,就必须确保它们是对的!错误地使用优化子句可能导致后端崩溃、微妙错误的输出或其他糟糕后果。如果不确定,你随时可以省略某个优化子句;唯一的后果是查询可能比所需的更慢。
未来的 Postgres 版本可能会添加更多优化子句。这里描述的是 6.5 版理解的所有子句。
如果给出 COMMUTATOR 子句,它指定一个与正在定义的操作符互为交换子的操作符。若对所有可能的输入值 x、y,都有 (x A y) 等于 (y B x),则称操作符 A 是操作符 B 的交换子。注意,B 也同样是 A 的交换子。例如,对某种特定数据类型而言,'<' 和 '>' 操作符通常互为交换子,而操作符 '+' 通常与其自身可交换。但操作符 '-' 通常并不与任何操作符可交换。
被交换操作符的左参数类型与其交换子的右参数类型相同,反之亦然。因此,只要给出交换子操作符的名称, Postgres 就足以查找到交换子,这也正是 COMMUTATOR 子句需要提供的全部内容。
定义一个与自身可交换的操作符时,直接定义即可。但定义一对可交换操作符时,情况就稍微复杂一些:第一个要定义的操作符如何引用另一个尚未定义的操作符呢?这个问题有两种解决办法:
一种办法是在你定义的第一个操作符中省略 COMMUTATOR 子句,然后在第二个操作符的定义中给出它。由于 Postgres 知道可交换操作符成对出现,当它看到第二个定义时,会自动回过头来补填第一个定义中缺失的 COMMUTATOR 子句。
另一种更直接的办法是在两个定义中都包含 COMMUTATOR 子句。当 Postgres 处理第一个定义并发现 COMMUTATOR 引用了一个不存在的操作符时,系统会在系统的 pg_operator 表中为该操作符创建一个占位条目。这个占位条目只在操作符名称、左右参数类型和结果类型上拥有有效数据,因为那是 Postgres 此时所能推断出的全部。第一个操作符的目录条目会链接到这个占位条目。之后当你定义第二个操作符时,系统会用第二个定义中的额外信息更新该占位条目。如果你在占位条目被填充之前就尝试使用该占位操作符,只会得到一条错误消息。(注意:这一过程在 6.5 之前的 Postgres 版本中不能可靠工作,但现在它是推荐的做法。)
如果给出 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 子句条件:
field OP constant
(即针对当前操作符和某个特定常量值)。这会帮助优化器大致了解这种形式的 WHERE 子句会消掉多少行。(你可能在想:如果常量在左边会怎样?嗯,这正是 COMMUTATOR 的用途之一……)
编写新的限制选择率估算函数远远超出了本章的范围,但幸运的是,对你的许多操作符,你通常可以直接使用系统的标准估算器之一。标准的限制估算器有:
eqsel for =
neqsel for <>
intltsel for < or <=
intgtsel for > or >=
这些类别看起来可能有点奇怪,但仔细想想是有道理的。'=' 通常只会接受表中一小部分行;'<>' 通常只会排除一小部分行。'<' 接受的比例取决于给定常量落在该表列取值范围中的位置(而这恰好是 VACUUM ANALYZE 收集并提供给选择率估算器的信息)。对同一个比较常量,'<=' 接受的比例略大于 '<',但两者足够接近,不值得区分,尤其是因为我们反正也不太可能比粗略猜测做得更好。类似的说明也适用于 '>' 和 '>='。
对于选择率很高或很低的操作符,即使它们并非真正的相等或不相等,你也常常可以凑合使用 eqsel 或 neqsel。例如,正则表达式匹配操作符(~、~* 等)就使用了 eqsel,其假设是它们通常只会匹配表中一小部分条目。
如果给出 JOIN 子句,它指定该操作符的连接选择率估算函数。(注意,这里是函数名,而不是操作符名。) JOIN 子句只对返回 boolean 的二元操作符有意义。连接选择率估算器的作用,是猜测一对表中有多少比例的行会满足如下形式的 WHERE 子句条件:
table1.field1 OP table2.field2
(即针对当前操作符)。与 RESTRICT 子句一样,这也能极大地帮助优化器判断若干可能的连接顺序中哪一个可能耗时最少。
与前面一样,本章不会尝试解释如何编写连接选择率估算函数,而只是建议你在适用时使用标准估算器之一:
eqjoinsel for =
neqjoinsel for <>
intltjoinsel for < or <=
intgtjoinsel for > or >=
如果给出 HASHES 子句,它告诉系统可以对此操作符上的连接使用哈希连接方法。 HASHES 只对返回 boolean 的二元操作符有意义,而且实践中该操作符最好是某种数据类型上的相等操作。
哈希连接背后的假设是:只有当左值和右值被哈希到同一个哈希码时,连接操作 符才有可能返回 TRUE。如果两个值被放进不同的哈希桶,连接过程就根本不会去比较它们,这实际上隐含假定连接操作符的结果必定为 FALSE。因此,对于那些并不表示相等关系的操作符,指定 HASHES 永远没有意义。
事实上,仅逻辑上的相等也不够;该操作符最好表示纯粹的按位相等,因为哈希函数是按值的内存表示计算的,而不管这些位是什么含义。例如,时间间隔的相等就不是按位相等;时间间隔相等操作符认为两个时间间隔只要时长相同就相等,无论其端点是否一致。这意味着在时间间隔字段上使用 "=" 的连接,如果实现为哈希连接,得到的结果会与其他实现方式不同,因为本应匹配的很大一部分值对会被哈希到不同的值,永远不会被哈希连接比较。而如果优化器选择使用另一种连接,相等操作符判定相等的所有值对都会被找到。我们不想要这种不一致,因此我们没有把时间间隔的相等操作标记为可哈希。
还有一些与机器相关的情形可能让哈希连接出错。例如,如果你的数据类型是一个可能含有无关填充位的结构,把它的相等操作符标记为 HASHES 是不安全的。(除非也许你把其他操作符写成保证未用的位始终为零。)另一个例子是 FLOAT 数据类型用于哈希连接是不安全的。在符合 IEEE 浮点标准的机器上,负零和正零是不同的值(位模式不同),但按定义它们比较相等。因此,如果把浮点相等标记为 HASHES,负零和正零可能不会被哈希连接配对,而任何其他连接过程都会把它们配上。
归根结底:你可能只应对那些(可以)用 memcmp() 实现的相等操作符使用 HASHES。
如果给出 SORT 子句,它们告诉系统可以对此操作符上的连接使用归并连接方法。指定其一就必须同时指定另一个。当前操作符必须是某对数据类型上的相等操作,而 SORT1 和 SORT2 子句分别命名左边和右边数据类型的排序操作符('<' 操作符)。
归并连接基于这样的思想:把左右两边的表排好序,然后并行扫描它们。因此,两个数据类型都必须能够全序化,而且连接操作符必须只在处于排序次序中"相同位置"的值对上才可能成功。实践中这意味着连接操作符的行为必须像相等一样。但与哈希连接不同——哈希连接要求左右数据类型最好相同(或至少按位等价)——只要两个不同的数据类型在逻辑上兼容,就可以对它们做归并连接。例如, int2 对 int4 的相等操作符就是可归并连接的。我们只需要能把两个数据类型排成逻辑兼容次序的排序操作符。
指定归并排序操作符时,当前操作符和两个被引用的操作符都必须返回 boolean;SORT1 操作符的两个输入数据类型都必须等于当前操作符的左参数类型, SORT2 操作符的两个输入数据类型都必须等于当前操作符的右参数类型。(与 COMMUTATOR 和 NEGATOR 一样,这意味着只凭操作符名就足以指定操作符,而且如果你碰巧先定义了相等操作符、后定义其他操作符,系统也能够创建占位操作符条目。)
实践中你只应对 '=' 操作符写 SORT 子句,而且两个被引用的操作符应当总是命名为 '<'。试图对名称不是这些的操作符使用归并连接将导致不可救药的混乱,原因我们稍后就会看到。
对于你标记为可归并连接的操作符,还有一些附加限制。这些限制目前不会被 CREATE OPERATOR 检查,但如果其中任何一条不成立,归并连接在运行时可能失败:
可归并连接的相等操作符必须有交换子 (两个数据类型相同时就是它自身,不同时则是一个相关的相等操作符)。
必须存在与可归并连接操作符本身具有相同左右输入数据类型的 '<' 和 '>' 排序操作符。这些操作符必须命名为 '<' 和 '>';这件事你没有选择的余地,因为没有显式指定它们的条款。注意,如果左右数据类型不同,这两个操作符都不是任何一个 SORT 操作符。但它们最好与 SORT 操作符以兼容的方式对数据值排序,否则归并连接将无法工作。
译文有误、术语不当或页面显示问题,请到译文仓库 pgsty/pgdoc 报告译文问题。 英文原文本身的问题,请在当前版本的对应页面向上游反馈;上游不再修订已结束维护的版本。