pgsql.cc 提供对 postgresql.org 官网内容的中文翻译,由 Pigsty 团队维护。
到目前为止所描述的过程使我们能够定义新类型、新函数以及新操作符。然而,我们还不能在一种新数据类型的列上定义索引。为此,必须为该新数据类型定义一个操作符类。本节稍后将用一个示例说明这一概念:为 B-树索引方法定义一个新的操作符类,以便按绝对值升序存储和排序复数。
操作符类可以分组成操作符族,以展示语义兼容的类之间的关系。只涉及单一数据类型时,一个操作符类就已足够,因此我们先关注这种情况,然后再回到操作符族。
pg_am 表为每个索引方法(内部称为访问方法)保存一行。对表进行常规访问的支持内置于 PostgreSQL 中,但所有索引方法都在 pg_am 中描述。可以定义所需的接口例程,然后在 pg_am 中创建一行,从而添加新的索引方法 — 但这超出了本章的范围(参见第 51 章)。
索引方法的例程并不直接知道它将处理哪些数据类型。相反,一个操作符类标识了索引方法在处理特定数据类型时需要使用的那组操作。之所以称为操作符类,是因为它指定的一项内容就是可与索引一起使用的 WHERE 子句操作符集合(也就是能被转换成索引扫描条件的操作符)。操作符类还可以指定索引方法内部操作所需的一些支持函数,但这些函数并不直接对应任何可与索引一起使用的 WHERE 子句操作符。
可以为同一种数据类型和索引方法定义多个操作符类。这样就能为一种数据类型定义多套索引语义。例如,一个 B-树索引要求为其处理的每一种数据类型定义一种排序顺序。对于复数数据类型,也许既需要一个按复数绝对值排序的 B-树操作符类,也需要另一个按实部排序的操作符类,等等。通常,其中一个操作符类会被视为最常用,并标记为该数据类型在该索引方法上的默认操作符类。
同一个操作符类名可以用于多个不同的索引方法(例如,B-树和哈希索引方法都有名为 int4_ops 的操作符类),但每一个这样的类都是独立实体,必须分别定义。
与操作符类关联的操作符通过“策略号”来标识,用以表示每个操作符在其操作符类上下文中的语义。例如,B-树对键施加了严格的从小到大的顺序,因此像“小于”和“大于等于”这样的操作符,对 B-树来说就很重要。由于 PostgreSQL 允许用户定义操作符,PostgreSQL 不能仅凭操作符名称(例如 < 或 >=)就判断它是哪一类比较。取而代之的是,索引方法定义了一组“策略”,可以把它们看成是广义的操作符。每个操作符类都会说明,对于某种特定数据类型和某种索引语义解释,每一种策略分别对应哪个实际操作符。
B-树索引方法定义了五种策略,如表 35.2所示。
表 35.2. B-树策略
| Operation | Strategy Number |
|---|---|
| less than | 1 |
| less than or equal | 2 |
| equal | 3 |
| greater than or equal | 4 |
| greater than | 5 |
哈希索引只支持等值比较,因此它们只使用一种策略,如表 35.3所示。
表 35.3. 哈希策略
| Operation | Strategy Number |
|---|---|
| equal | 1 |
GiST 索引更加灵活:它们根本没有固定的策略集合。相反,每个特定 GiST 操作符类中负责“一致性”检查的支持例程会按自己的方式解释策略号。举例来说,一些内置的 GiST 索引操作符类会为二维几何对象建立索引,并提供“R 树”策略,如表 35.4所示。其中四个是真正的二维测试(重叠、相同、包含、被包含),四个只考虑 X 方向,另外四个则在 Y 方向上提供相同测试。
表 35.4. GiST Two-Dimensional “R-tree” Strategies
| Operation | Strategy Number |
|---|---|
| 严格位于左侧 | 1 |
| 不延伸到右侧 | 2 |
| 重叠 | 3 |
| 不延伸到左侧 | 4 |
| 严格位于右侧 | 5 |
| 相同 | 6 |
| 包含 | 7 |
| 被包含 | 8 |
| 不延伸到上方 | 9 |
| 严格位于下方 | 10 |
| 严格位于上方 | 11 |
| 不延伸到下方 | 12 |
GIN 索引在灵活性方面与 GiST 索引类似:它们没有固定的策略集合。 相反,每个操作符类的支持例程会根据该操作符类的定义解释策略号。 举例来说,内置数组操作符类所使用的策略号如 表 35.5 所示。
表 35.5. GIN 数组策略
| Operation | Strategy Number |
|---|---|
| overlap | 1 |
| 包含 | 2 |
| is contained by | 3 |
| equal | 4 |
注意,所有策略操作符都返回布尔值。实际上,所有被定义为索引方法 策略的操作符都必须返回 boolean 类型,因为要与索引 配合使用,它们必须出现在 WHERE 子句的顶层。
仅靠策略信息通常不足以让系统知道如何使用索引。实际上,索引方法还需要额外的支持例程才能工作。例如,B-树索引方法必须能够比较两个键,并判断其中一个是大于、等于还是小于另一个。类似地,哈希索引方法必须能够为键值计算哈希码。这些操作并不对应 SQL 命令条件中使用的操作符;它们是索引方法内部使用的管理例程。
与策略一样,操作符类会标识对于给定的数据类型和语义解释,应由哪些具体函数承担这些角色。索引方法定义它需要的函数集合,而操作符类则会通过为函数分配由索引方法规定的“支持函数号”来标识正确的函数。
B-tree 要求一个支持函数,如 表 35.6 所示。
表 35.6. B-树支持函数
| 函数 | 支持号 |
|---|---|
| 比较两个键,并返回一个小于零、等于零或大于零的整数,用以 表示第一个键是小于、等于还是大于第二个键 | 1 |
哈希索引同样只需要一个支持函数,如 表 35.7 所示。
表 35.7. 哈希支持函数
| 函数 | 支持号 |
|---|---|
| 计算一个键的哈希值 | 1 |
GiST 索引要求七个支持函数,如 表 35.8 所示。
表 35.8. GiST 支持函数
| 函数 | 支持号 |
|---|---|
| consistent - 确定键是否满足查询条件 | 1 |
| union - 计算一组键的并集 | 2 |
| compress - 计算将被索引的键或值的压缩表示 | 3 |
| decompress - 计算压缩键的解压表示 | 4 |
| penalty - 计算把新键插入具有给定子树键的子树时的罚值 | 5 |
| picksplit - 确定页面中的哪些项要移到新页面,并计算 结果页面的并集键 | 6 |
| equal - 比较两个键,并在它们相等时返回真 | 7 |
GIN 索引要求四个支持函数,如 表 35.9 所示。
表 35.9. GIN 支持函数
| 函数 | 描述 | 支持号 |
|---|---|---|
compare |
比较两个键,并返回一个小于零、等于零或大于零的整数,用以 表示第一个键是小于、等于还是大于第二个键 | 1 |
extractValue |
从待索引值中提取键 | 2 |
extractQuery |
从查询条件中提取键 | 3 |
consistent |
确定值是否匹配查询条件 | 4 |
comparePartial |
(可选方法)比较查询中的部分键与索引中的键,并返回一个 小于零、等于零或大于零的整数,用以指示 GIN 应忽略该索引项、 将该项视为匹配,还是停止索引扫描 | 5 |
与策略操作符不同,支持函数返回的是特定索引方法所期望的数据 类型;例如,对 B-tree 的比较函数来说,就是一个有符号整数。每个 支持函数的参数个数和类型也同样取决于索引方法。对于 B-tree 和 哈希,支持函数接受的输入数据类型与操作符类中包含的操作符相同, 但对大多数 GIN 和 GiST 支持函数来说并非如此。
现在我们已经了解了这些基本思想,下面给出先前承诺的创建新操作符类示例。(这个可运行示例位于源码发布包中的src/tutorial/complex.c和src/tutorial/complex.sql。)该操作符类封装了一组按绝对值顺序对复数排序的操作符,因此我们把它命名为complex_abs_ops。首先,我们需要一组操作符。定义操作符的过程已经在第 35.12 节中讨论过。对于 B-树上的操作符类,我们需要如下操作符:
定义一组相关比较操作符时,最不容易出错的方式是先编写 B-树比较支持函数,再把其他函数写成围绕该支持函数的一行包装器函数。这样可以降低在边界情况下得到不一致结果的概率。按照这种方法,我们首先编写:
#define Mag(c) ((c)->x*(c)->x + (c)->y*(c)->y)
static int
complex_abs_cmp_internal(Complex *a, Complex *b)
{
double amag = Mag(a),
bmag = Mag(b);
if (amag < bmag)
return -1;
if (amag > bmag)
return 1;
return 0;
}
现在,小于函数如下所示:
PG_FUNCTION_INFO_V1(complex_abs_lt);
Datum
complex_abs_lt(PG_FUNCTION_ARGS)
{
Complex *a = (Complex *) PG_GETARG_POINTER(0);
Complex *b = (Complex *) PG_GETARG_POINTER(1);
PG_RETURN_BOOL(complex_abs_cmp_internal(a, b) < 0);
}
其他四个函数的区别只在于它们如何比较内部函数的结果与 0。
接下来,在 SQL 中声明这些函数,以及基于这些函数的操作符:
CREATE FUNCTION complex_abs_lt(complex, complex) RETURNS bool
AS 'filename', 'complex_abs_lt'
LANGUAGE C IMMUTABLE STRICT;
CREATE OPERATOR < (
leftarg = complex, rightarg = complex, procedure = complex_abs_lt,
commutator = > , negator = >= ,
restrict = scalarltsel, join = scalarltjoinsel
);
必须指定正确的交换子和求反器操作符,以及合适的限制选择率与连接选择率函数,否则优化器无法有效使用索引。注意,小于、等于和大于这几种情况应使用不同的选择率函数。
这里还有几点值得注意:
只能有一个名为 = 且两个操作数都为 complex 类型的操作符。在这个例子里,我们并没有任何其他 = 操作符可用于 complex;但如果我们是在构造一种实际使用的数据类型,可能会希望 = 表示复数的普通相等,而不是绝对值相等。在那种情况下,我们就需要为 complex_abs_eq 选用其他操作符名。
尽管 PostgreSQL 能处理 SQL 名称相同但参数数据类型不同的函数,C 却只能处理给定名称的一个全局函数。因此,我们不应该把 C 函数简单命名成 abs_eq 之类。通常,在 C 函数名中包含数据类型名称是个好习惯,这样就不会与其他数据类型的函数发生冲突。
我们原本也可以把该函数的 SQL 名称取为 abs_eq,并依靠 PostgreSQL 通过参数数据类型把它与其他同名 SQL 函数区分开。为了让示例保持简单,这里我们让 C 层和 SQL 层的函数使用相同的名称。
下一步是注册 B-树要求的支持例程。实现该例程的 C 示例代码与操作符函数位于同一个文件中。该函数的声明如下:
CREATE FUNCTION complex_abs_cmp(complex, complex)
RETURNS integer
AS 'filename'
LANGUAGE C IMMUTABLE STRICT;
现在我们已经有了所需的操作符和支持例程,就可以最终创建操作符类:
CREATE OPERATOR CLASS complex_abs_ops
DEFAULT FOR TYPE complex USING btree AS
OPERATOR 1 < ,
OPERATOR 2 <= ,
OPERATOR 3 = ,
OPERATOR 4 >= ,
OPERATOR 5 > ,
FUNCTION 1 complex_abs_cmp(complex, complex);
这样就完成了!现在应该可以在 complex 列上创建并使用 B-树索引。
我们本来也可以把操作符项写得更详细一些,例如:
OPERATOR 1 < (complex, complex) ,
但是当操作符接受的数据类型与该操作符类所服务的数据类型相同时,就没有必要这样写。
上述示例假定你希望把这个新操作符类设为 complex 数据类型的默认 B-树操作符类。如果不是这样,只需省去 DEFAULT 这个词。
到目前为止,我们一直隐含地假定一个操作符类只处理一种数据类型。虽然某个特定的索引列当然只能有一种数据类型,但对把被索引列与另一种数据类型的值进行比较的操作建立索引往往也很有用。此外,如果某个与操作符类相关的跨数据类型操作符有用,通常另一种数据类型本身也会有一个相关的操作符类。把相关类之间的联系显式表示出来会很有帮助,因为这有助于规划器优化 SQL 查询(尤其是对 B-树操作符类而言,因为规划器中包含大量有关如何使用它们的知识)。
为了满足这些需求,PostgreSQL使用操作符族这一概念。一个操作符族包含一个或多个操作符类,还可以包含属于整个族、但不属于族中任何单一类的可索引操作符及其相应的支持函数。我们称这样的操作符和函数在该族中是“松散”的,而不是绑定在某个特定类中。通常,每个操作符类只包含单一数据类型的操作符,而跨数据类型操作符则作为操作符族中的松散成员存在。
一个操作符族中的所有操作符和函数都必须具有兼容的语义,而兼容性的要求由索引方法设定。因此,你也许会疑惑,为什么还要把该族的某些子集单独划成操作符类;事实上,对很多用途而言,类的划分并不重要,真正有意义的分组只有操作符族。之所以定义操作符类,是因为它们规定了支持特定索引所需的操作符族内容。如果某个索引使用了某个操作符类,那么在不删除该索引的情况下就不能删除该操作符类 — 但操作符族中的其他部分,也就是其他操作符类和松散操作符,则可以被删除。因此,一个操作符类应当只包含在特定数据类型上支持索引所合理需要的最小操作符和函数集合,而那些相关但非必需的操作符,则可以作为操作符族的松散成员加入。
As an example, PostgreSQL has a built-in B-tree operator family integer_ops, which includes operator classes int8_ops, int4_ops, and int2_ops for indexes on bigint (int8), integer (int4), and smallint (int2) 列上的索引。该族还包含跨数据类型比较操作符,允许这些类型中的 任意两个相互比较,这样其中一种类型上的索引就可以用另一种类型的 比较值来搜索。该族可以用这些定义复制出来:
CREATE OPERATOR FAMILY integer_ops USING btree; CREATE OPERATOR CLASS int8_ops DEFAULT FOR TYPE int8 USING btree FAMILY integer_ops AS -- standard int8 comparisons OPERATOR 1 < , OPERATOR 2 <= , OPERATOR 3 = , OPERATOR 4 >= , OPERATOR 5 > , FUNCTION 1 btint8cmp(int8, int8) ; CREATE OPERATOR CLASS int4_ops DEFAULT FOR TYPE int4 USING btree FAMILY integer_ops AS -- standard int4 comparisons OPERATOR 1 < , OPERATOR 2 <= , OPERATOR 3 = , OPERATOR 4 >= , OPERATOR 5 > , FUNCTION 1 btint4cmp(int4, int4) ; CREATE OPERATOR CLASS int2_ops DEFAULT FOR TYPE int2 USING btree FAMILY integer_ops AS -- standard int2 comparisons OPERATOR 1 < , OPERATOR 2 <= , OPERATOR 3 = , OPERATOR 4 >= , OPERATOR 5 > , FUNCTION 1 btint2cmp(int2, int2) ; ALTER OPERATOR FAMILY integer_ops USING btree ADD -- cross-type comparisons int8 vs int2 OPERATOR 1 < (int8, int2) , OPERATOR 2 <= (int8, int2) , OPERATOR 3 = (int8, int2) , OPERATOR 4 >= (int8, int2) , OPERATOR 5 > (int8, int2) , FUNCTION 1 btint82cmp(int8, int2) , -- cross-type comparisons int8 vs int4 OPERATOR 1 < (int8, int4) , OPERATOR 2 <= (int8, int4) , OPERATOR 3 = (int8, int4) , OPERATOR 4 >= (int8, int4) , OPERATOR 5 > (int8, int4) , FUNCTION 1 btint84cmp(int8, int4) , -- cross-type comparisons int4 vs int2 OPERATOR 1 < (int4, int2) , OPERATOR 2 <= (int4, int2) , OPERATOR 3 = (int4, int2) , OPERATOR 4 >= (int4, int2) , OPERATOR 5 > (int4, int2) , FUNCTION 1 btint42cmp(int4, int2) , -- cross-type comparisons int4 vs int8 OPERATOR 1 < (int4, int8) , OPERATOR 2 <= (int4, int8) , OPERATOR 3 = (int4, int8) , OPERATOR 4 >= (int4, int8) , OPERATOR 5 > (int4, int8) , FUNCTION 1 btint48cmp(int4, int8) , -- cross-type comparisons int2 vs int8 OPERATOR 1 < (int2, int8) , OPERATOR 2 <= (int2, int8) , OPERATOR 3 = (int2, int8) , OPERATOR 4 >= (int2, int8) , OPERATOR 5 > (int2, int8) , FUNCTION 1 btint28cmp(int2, int8) , -- cross-type comparisons int2 vs int4 OPERATOR 1 < (int2, int4) , OPERATOR 2 <= (int2, int4) , OPERATOR 3 = (int2, int4) , OPERATOR 4 >= (int2, int4) , OPERATOR 5 > (int2, int4) , FUNCTION 1 btint24cmp(int2, int4) ;
注意,此定义“重载”了操作符策略号和支持函数号:每个 编号在操作符族中出现多次。只要同一编号的每个实例具有不同的输入 数据类型,这就是允许的。两个输入类型都等于某个操作符类输入类型 的实例,是该操作符类的主要操作符和支持函数,通常应声明为操作符 类的一部分,而不是操作符族的松散成员。
在一个 B-tree 操作符族中,该族中的所有操作符都必须以兼容的方式 排序,也就是说,传递律必须适用于该族支持的所有数据类型: “如果 A = B 且 B = C,则 A = C”,以及 “如果 A < B 且 B < C,则 A < C”。对族中的每一个 操作符,都必须有一个具有相同两个输入数据类型的支持函数。建议 让操作符族保持完整,也就是说,对每一种数据类型组合都应包含全部 操作符。每个操作符类只应包含其数据类型对应的非跨数据类型操作符 和支持函数。
要构建一个多数据类型的哈希操作符族,必须为该族支持的每一种数据 类型创建相互兼容的哈希支持函数。这里的兼容性是指:对任意两个被 该族中的等值操作符视为相等的值,这些函数都保证返回相同的哈希码, 即使这两个值属于不同类型也是如此。当这些类型具有不同的物理表示 时,这通常难以实现,但在某些情况下可以做到。注意,每种数据类型 只有一个支持函数,而不是每个等值操作符一个。建议让操作符族保持 完整,也就是说,对每一种数据类型组合都提供一个等值操作符。每个 操作符类只应包含其数据类型对应的非跨数据类型等值操作符和支持 函数。
GIN 和 GiST 索引没有任何显式的跨数据类型操作概念。它们所支持 的操作符集合,就是给定操作符类的主要支持函数所能处理的那些 操作符。
在 PostgreSQL 8.3 之前,并没有操作符族这一概念,因此任何打算与索引一起使用的跨数据类型操作符都必须直接绑定到该索引的操作符类中。虽然这种做法仍然有效,但已被弃用,因为它会使索引的依赖关系过于宽泛,而且当两种数据类型都在同一操作符族中拥有操作符时,规划器能更有效地处理跨数据类型比较。
PostgreSQL利用操作符类来从多方面推断操作符的属性,而不仅仅是判断它们能否用于索引。因此,即便你并不打算为自己的数据类型列建立索引,也可能会想创建操作符类。
特别地,ORDER BY和DISTINCT等 SQL 特性要求对值的比较和排序。为了在用户定义的数据类型上实现这些特性,PostgreSQL会为数据类型查找默认 B-树操作符类。这个操作符类的“相等”成员定义了用于GROUP BY和DISTINCT的值的等值概念,而该操作符类施加的排序顺序定义了默认的ORDER BY顺序。
用户定义类型的数组比较也依赖于该类型默认 B-树操作符类所定义的语义。
如果一种数据类型没有默认的 B-树操作符类,系统就会查找默认的哈希操作符类。但由于这类操作符类只提供等值语义,因此在实践中它只足以支持数组相等比较。
如果某种数据类型没有默认操作符类,而你又试图将这些 SQL 特性用于该数据类型,就会得到类似“无法识别排序操作符”这样的错误。
在版本 7.4 以前的PostgreSQL中,排序和分组操作将隐式地使用名为=、<以及>的操作符。新的依赖于默认操作符类的行为避免了对具有特定名字的操作符行为作出任何假设。
另一个重要点在于,出现在哈希操作符族中的操作符,都是哈希连接、哈希聚合以及相关优化的候选对象。这里哈希操作符族至关重要,因为它标识了应当使用的哈希函数。
还有两个操作符类的特殊特性我们尚未讨论,主要是因为它们对最常用的索引方法没有用处。
通常,把一个操作符声明为操作符类(或操作符族)的成员,意味着 索引方法能够正好检索出满足使用该操作符的 WHERE 条件的行集。例如:
SELECT * FROM table WHERE integer_column < 4;
可以由整数列上的 B-tree 索引精确满足。但也存在这样的情况:索引 只是查找匹配行的不精确向导。例如,如果一个 GiST 索引只为几何对象存储包围盒,那么它就无法精确满足测试多边形 等非矩形对象之间重叠的 WHERE 条件。不过,我们可以用 索引找出包围盒与目标对象的包围盒重叠的对象,然后只对索引找到的 对象做精确的重叠测试。如果适用这种情形,就称该索引对该操作符是 “有损”的。有损索引搜索的实现方式是:当某行可能真正 满足、也可能不满足查询条件时,由索引方法返回一个 recheck(重新检查)标志。随后,核心系统会 在检索到的行上测试原始查询条件,看它是否应作为有效匹配返回。只要 索引保证返回所有需要的行(可能还外加一些额外的行,它们可以通过 执行原始操作符调用来排除),这种方法就是可行的。支持有损搜索的 索引方法(目前是 GiST 和 GIN)允许各个操作符类的支持函数设置 recheck 标志,因此这实质上是操作符类层面的特性。
再次考虑只在索引中存储多边形等复杂对象的包围盒的情况。此时,在索引条目中存储整个多边形没有多少价值,不如只存储一个更简单的对象,其类型为box。这种情况由STORAGE选项表达,该选项位于CREATE OPERATOR CLASS:可以写成如下形式:
CREATE OPERATOR CLASS polygon_ops
DEFAULT FOR TYPE polygon USING gist AS
...
STORAGE box;
目前,只有 GiST 和 GIN 索引方法支持与列数据类型不同的STORAGE类型。GiST 的compress和decompress支持函数在使用STORAGE时必须处理数据类型转换。在 GIN 中,STORAGE类型标识“键”值的类型,通常与被索引列的类型不同。例如,整数数组列的操作符类可以只使用整数作为键。GIN 的extractValue和extractQuery支持函数负责从被索引值中提取键。
译文有误、术语不当或页面显示问题,请到译文仓库 pgsty/pgdoc 报告译文问题。 英文原文本身的问题,请在当前版本的对应页面向上游反馈;上游不再修订已结束维护的版本。