迄今为止已经描述的过程让我们能够定义新的类型、新的函数以及新的操作符。但是,我们还不能在一种新数据类型的列上定义索引。要做这件事情,我们必须为新数据类型定义一个操作符类。在这一小节稍后的部分,我们将用一个例子阐述这部份内容:一个用于 B-树索引方法的操作符类,它以绝对值的升序存储和排序复数。
操作符类可以被分组成操作符族来体现语义兼容的类之间的联系。当只涉及到一种单一数据类型时,一个操作符类就足矣。因此我们将先把重点放在这种情况上,然后再回到操作符族。
pg_am
表为每一种索引方法都包含一行(内部被称为访问方法)。LightDB中内建了对表常规访问的支持,但是所有的索引方法则是在pg_am
中描述。可以通过编写必要的代码并且在pg_am
中创建一项来增加一种新的索引访问方法 — 但这超出了本章的范围。
一个索引方法的例程并不直接了解它将要操作的数据类型。而是由一个操作符类标识索引方法用来操作一种特定数据类型的一组操作。之所以被称为操作符类是因为它们指定的一件事情就是可以被用于一个索引的WHERE
子句操作符集合(即,能被转换成一个索引扫描条件)。一个操作符类也能指定一些索引方法内部操作所需的支持函数,这些过程不能直接对应于能用于索引的任何WHERE
子句操作符。
可以为相同的数据类型和索引方法定义多个操作符类。通过这种方式,可以为一种数据类型定义多个索引语义集合。例如,一个B-树索引要求在它要操作的每一种数据类型上都定义一个排序顺序。对一种复数数据类型来说,拥有一个可以根据复数绝对值排序的 B-树操作符类和另一个可以根据实数部分排序的操作符类可能会有用。典型地,其中一个操作符类将被认为是最常用的并且将被标记为那种数据类型和索引方法的默认操作符类。
相同的操作符类名称可以被用于多个不同的索引方法(例如,B-树和哈希索引方法都有名为int4_ops
的操作符类)。但是每一个这样的类都是一个独立实体并且必须被单独定义。
与一个操作符类关联的操作符通过“策略号”标识,它被用来标识每个操作符在其操作符类中的语义。例如,B-树在键上施行了一种严格的顺序(较小到较大),因此“小于”和“大于等于”这样的操作符就是 B-树所感兴趣的。因为LightDB允许用户定义操作符,LightDB不能看着一个操作符(如<
和>=
)的名字并且说出它是哪一种比较。取而代之的是,索引方法定义了一个“策略”集合,它们可以被看成是广义的操作符。每一个操作符类会说明对于一种特定的数据类型究竟是哪个实际的操作符对应于每一种策略以及该索引语义的解释。
B-树索引方法定义了五种策略,如Table 38.3所示。
Table 38.3. B-树策略
操作 | 策略号 |
---|---|
小于 | 1 |
小于等于 | 2 |
等于 | 3 |
大于等于 | 4 |
大于 | 5 |
哈希索引只支持等值比较,因此它们只使用一种策略,如Table 38.4所示。
Table 38.4. 哈希策略
操作 | 策略号 |
---|---|
等于 | 1 |
GIN 没有一个固定的策略集合。取而代之的是,每一个操作符类的支持例程负责根据该操作符类的定义解释策略号。例如,被内建操作符类用于数组的策略号如Table 38.5所示。
Table 38.5. GIN 数组策略
操作 | 策略号 |
---|---|
重叠 | 1 |
包含 | 2 |
被包含 | 3 |
等于 | 4 |
注意上文列出的所有操作符都返回布尔值。实际上,所有作为索引方法搜索操作符定义的操作符必须返回类型boolean
,因为它们必须出现在一个WHERE
子句的顶层来与一个索引一起使用。
对于系统来说只有策略信息通常不足以断定如何使用一种索引。实际上,为了能工作,索引方法还要求额外的支持例程。例如,B-树索引方法必须能比较两个键并且决定其中一个是否大于、等于或小于另一个。类似地,哈希索引方法必须能够为键值计算哈希码。这些操作并不对应在 SQL 命令的条件中使用的操作符。它们是索引方法在内部使用的管理例程。
与策略一样,操作符类会标识哪些函数应该为一种给定的数据类型扮演这些角色以及相应的语义解释。索引方法定义它需要的函数集合,而操作符类则会通过为函数分配由索引方法说明的“支持函数号”来标识正确的函数。
此外,一些 opclass 允许用户指定控制其行为的参数。每个内置索引访问方法都有一个可选的
options
支持函数,它定义了一组特定于 opclass 的参数。
如Table 38.6所示, B-树要求一个比较支持函数,并且允许在操作符类作者的选项中提供四个额外的支持函数。
Table 38.6. B-树支持函数
函数 | 支持号 |
---|---|
比较两个键并且返回一个小于零、等于零或大于零的整数,它表示第一个键小于、等于或者大于第二个键。 | 1 |
返回C可调用的排序支持函数的地址(可选)。 | 2 |
将一个测试值与一个基础值加上/减去一个偏移量的结果进行比较,根据比较的结果返回真或假(可选) | 3 |
确定使用运算符类应用 btree 重复数据删除优化的索引是否安全(可选) | 4 |
定义一组特定于此运算符类的选项(可选) | 5 |
如Table 38.7所示,哈希索引要求一个支持函数,并且允许在操作符类作者的选项中提供两个额外的支持函数。
Table 38.7. 哈希支持函数
函数 | 支持号 |
---|---|
为一个键计算32位哈希值 | 1 |
给定一个64-位salt,计算一个键的64位哈希值。如果salt为0,结果的低32位必须匹配会由函数1计算出来的值(可选) | 2 |
定义一组特定于此运算符类的选项(可选) | 3 |
如Table 38.8所示,GIN 索引有七个支持函数,其中四个是可选的。
Table 38.8. GIN 支持函数
函数 | 描述 | 支持号 |
---|---|---|
compare | 比较两个键并且返回一个小于零、等于零或大于零的整数,它表示第一个键小于、等于或者大于第二个键 | 1 |
extractValue | 从一个要被索引的值中抽取键 | 2 |
extractQuery | 从一个查询条件中抽取键 | 3 |
consistent | 判断值是否匹配查询条件(布尔变体)(如果支持函数 6 存在则是可选的) | 4 |
comparePartial | 比较来自查询的部分键和来自索引的键,并且返回一个小于零、等于零或大于零的整数,表示 GIN 是否应该忽略该索引项、把该项当做一个匹配或者停止索引扫描(可选) | 5 |
triConsistent | 判断值是否匹配查询条件(三元变体)(如果支持函数 4 存在则是可选的) | 6 |
options | 定义一组特定于此运算符类的选项(可选) | 7 |
和搜索操作符不同,支持函数返回特定索引方法所期望的数据类型,例如在 B 树的比较函数中是一个有符号整数。每个支持函数的参数数量和类型也取决于索引方法。对于 B 树和哈希,比较和哈希支持函数和包括在操作符类中的操作符接收一样的输入数据类型,但是大部分 GIN 支持函数则不是这样。
现在我们已经看过了基本思想,这里是创建一个新操作符类的例子(可以在源代码的src/tutorial/complex.c
和src/tutorial/complex.sql
中找到这个例子)。该操作符类封装了以绝对值顺序排序复数的操作符,因此我们为它取名为complex_abs_ops
。首先,我们需要一个操作符集合。定义操作符的过程已经在Section 38.14中讨论过。对于一个 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
使用某种其他的操作符名称。
尽管LightDB能够处理具有相同 SQL 名称的函数(只要它们具有不同的参数数据类型),但 C 只能处理具有给定名称一个全局函数。因此,我们不能简单地把 C 函数命名为abs_eq
之类的东西。通常,在 C 函数名中包括数据类型的名称是一种好习惯,这样就不会与其他数据类型的函数发生冲突。
我们可以让函数也具有abs_eq
这样的 SQL 名称,而依靠LightDB通过参数数据类型来区分它和其他同名 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-树操作符类,因为优化器包含了大量有关如何使用它们的知识)。
为了处理这些需求,LightDB使用了操作符族的概念。一个操作符族包含一个或者多个操作符类,并且也能包含属于该族整体而不属于该族中任何单一类的可索引操作符和相应的支持函数。我们说这样的操作符和函数是“松散地”存在于该族中,而不是被绑定在一个特定的类中。通常每个操作符类包含单一数据类型的操作符,而跨数据类型操作符则松散地存在于操作符族中。
一个操作符族中的所有操作符和函数必须具有兼容的语义,其中的兼容性要求由索引方法设定。你可能因此而奇怪为什么要这么麻烦地把族的特定子集单另出来成为操作符类,并且实际上(由于很多原因)这种划分与操作符之间没有什么直接的关联,只有操作符族才是实际的分组。定义操作符类的原因是,它们指定了特定索引对操作符族的依赖程度。如果一个索引使用着一个操作符类,那么不删除该索引是不能删除该操作符类的 — 但是操作符族的其他部分(即其他操作符类和松散的操作符)可以被删除。因此,一个操作符类应该包含一个索引在特定数据类型上正常工作所需要的最小操作符和函数集合,而相关但不关键的操作符可以作为操作符族的松散成员被加入。
例如,LightDB有一个内建的 B-树操作符族integer_ops
,它包括分别用于类型bigint
(int8
)、integer
(int4
)和smallint
(int2
)列上索引的操作符类int8_ops
、int4_ops
以及int2_ops
。这个族也包含跨数据类型比较操作符,它们允许对这些类型中的任意两种进行比较,这样可以通过一种类型的比较值来搜索另一种类型之上的索引。这个族可以用这些定义来重现:
CREATE OPERATOR FAMILY integer_ops USING btree; CREATE OPERATOR CLASS int8_ops DEFAULT FOR TYPE int8 USING btree FAMILY integer_ops AS -- 标准 int8 比较 OPERATOR 1 < , OPERATOR 2 <= , OPERATOR 3 = , OPERATOR 4 >= , OPERATOR 5 > , FUNCTION 1 btint8cmp(int8, int8) , FUNCTION 2 btint8sortsupport(internal) , FUNCTION 3 in_range(int8, int8, int8, boolean, boolean) , FUNCTION 4 btequalimage(oid) ; CREATE OPERATOR CLASS int4_ops DEFAULT FOR TYPE int4 USING btree FAMILY integer_ops AS -- 标准 int4 比较 OPERATOR 1 < , OPERATOR 2 <= , OPERATOR 3 = , OPERATOR 4 >= , OPERATOR 5 > , FUNCTION 1 btint4cmp(int4, int4) , FUNCTION 2 btint4sortsupport(internal) , FUNCTION 3 in_range(int4, int4, int4, boolean, boolean) , FUNCTION 4 btequalimage(oid) ; CREATE OPERATOR CLASS int2_ops DEFAULT FOR TYPE int2 USING btree FAMILY integer_ops AS -- 标准 int2 比较 OPERATOR 1 < , OPERATOR 2 <= , OPERATOR 3 = , OPERATOR 4 >= , OPERATOR 5 > , FUNCTION 1 btint2cmp(int2, int2) , FUNCTION 2 btint2sortsupport(internal) , FUNCTION 3 in_range(int2, int2, int2, boolean, boolean) , FUNCTION 4 btequalimage(oid) ; ALTER OPERATOR FAMILY integer_ops USING btree ADD -- 跨类型比较 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) , -- 跨类型比较 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) , -- 跨类型比较 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) , -- 跨类型比较 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) , -- 跨类型比较 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) , -- 跨类型比较 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) , -- 跨类型的in_range函数 FUNCTION 3 in_range(int4, int4, int8, boolean, boolean) , FUNCTION 3 in_range(int4, int4, int2, boolean, boolean) , FUNCTION 3 in_range(int2, int2, int8, boolean, boolean) , FUNCTION 3 in_range(int2, int2, int4, boolean, boolean) ;
注意这种定义“重载”了操作符策略和支持函数号:每一个编号在该族中出现多次。只要一个特定编号的每一个实例都有可区分的输入数据类型,就允许这样做。输入类型等于操作符类输入类型的实例是该操作符类的主要操作符和支持函数,并且在大部分情况下应该被声明为该操作符类的一部分而不是作为操作符族的松散成员存在。
在一个 B-树操作符族中,所有该族中的操作符必须以兼容的方式排序。对该族中的每一个操作符都必须有一个与该操作符具有相同的两个输入数据类型的支持函数。我们推荐让操作符族保持完整,即对每一种数据类型的组合都应该包括所有的操作符。每个操作符类只应该包括非跨类型操作符和用于其数据类型的支持函数。
为了构建一个多数据类型的哈希操作符族,必须为该族支持的每一种数据类型创建相兼容的哈希支持函数。这里的兼容性是指这些函数对于任意两个被该族中等值操作符认为相等的值会保证返回相同的哈希码,即便这些值具有不同的类型时也是如此。当这些类型具有不同的物理表示时,这通常难以实现,但是在某些情况下是可以做到的。此外,将该操作符族中一种数据类型的值通过隐式或者二进制强制造型转换成该族中另一种数据类型时,不应该改变所计算出的哈希值。注意每种数据类型只有一个支持函数,而不是每个等值操作符一个。我们推荐让操作符族保持完整,即对每一种数据类型的组合提供一个等值操作符。每个操作符类只应该包括非跨类型等值操作符和用于其数据类型的支持函数。
GIN 索引没有任何明显的跨数据类型操作的概念。它们所支持的操作符集合就是一个给定操作符类能够处理的主要支持函数。
在LightDB 8.3 之前,没有操作符族的概念,并且因此要在索引中使用的任何跨数据类型操作符必须被直接绑定到该索引的操作符类中。虽然这种方法仍然有效,但是已被废弃,因为它会让索引的依赖过于广泛,还因为当两种数据类型都在同一操作符族中有操作符时优化器可以更有效地处理跨数据类型比较。
LightDB使用操作符类来以更多方式推断操作符的属性,而不仅仅是它们是否能被用于索引。因此,即便不准备对你的数据类型的列建立索引,也可能想要创建操作符类。
特别地,ORDER BY
和DISTINCT
等 SQL 特性要求对值的比较和排序。为了在用户定义的数据类型上实现这些特性,LightDB会为数据类型查找默认 B-树操作符类。这个操作符类的“equals”成员定义了用于GROUP BY
和DISTINCT
的值的等值概念,而该操作符类施加的排序顺序定义了默认的ORDER BY
顺序。
如果一种数据类型没有默认的 B-树操作符类,系统将查找默认的哈希操作符类。但由于这类操作符类只提供等值,所以它只能支持分组而不能支持排序。
在一种数据类型没有默认操作符类时,如果尝试对该数据类型使用这些 SQL 特性,你将得到类似“could not identify an ordering operator”(无法标识排序操作符)的错误。
在版本 7.4 以前的LightDB中,排序和分组操作将隐式地使用名为=
、<
以及>
的操作符。新的依赖于默认操作符类的行为避免了对具有特定名字的操作符行为作出任何假设。
通过在一个USING
选项中指定一个非默认B-树操作符类的小于操作符,可以使用该操作符进行排序,例如
SELECT * FROM mytable ORDER BY somecol USING ~<~;
或者,在USING
中指定该操作符类的大于操作符可以选择升序的排序。
用户定义类型的数组的比较还依赖于该类型的默认B-树操作符类所定义的语义。如果没有默认的B-树操作符类,但有一个默认的哈希操作符类,则支持数组的相等比较,但不支持顺序的比较。
另一种要求更多数据类型相关知识的SQL特性是窗口函数(见Section 5.2.8)的RANGE
offset
PRECEDING
/FOLLOWING
帧选项。对于这样的一个查询
SELECT sum(x) OVER (ORDER BY x RANGE BETWEEN 5 PRECEDING AND 10 FOLLOWING) FROM mytable;
不足以了解如何用x
进行排序,数据库还必须理解如何对当前行的x
值“减5”或者“加10”以标识当前窗口帧的边界。把得到的边界与其他行的x
值用B-树操作符类提供的比较操作符(定义了ORDER BY
顺序)进行比较是可能的 — 但是加和减操作符并不是该操作符类的一部分,因此应该用哪些操作符呢?硬编码的选择是不切实际的,因为不同的排序顺序(不同的B-树操作符)可能需要不同的行为。因此,一个B-树操作符类可以指定一个in_range支持函数,它封装有对排序顺序有意义的加和减行为。如果有多种数据类型可以用作RANGE
子句中的偏移量,甚至可以提供多个in_range支持函数。如果与窗口的ORDER BY
子句关联的B-树操作符类没有一个匹配的in_range支持函数,则不支持RANGE
offset
PRECEDING
/FOLLOWING
选项。
另一个要点是,出现在一个哈希操作符族中的操作符是哈希连接、哈希聚集和相关优化的候选。这些情况下哈希操作符族就是至关重要的,因为它标识了要使用的哈希函数。
有两个操作符类的特性我们还没有讨论,主要是因为它们对于最常用的索引方法不太有用。
通常,把一个操作符声明为一个操作符类(或操作符族)的成员意味着该索引方法能够使用该操作符准确地检索满足WHERE
条件的行集。例如:
SELECT * FROM table WHERE integer_column < 4;
恰好可以被该整数列上一个 B-树索引满足。但是也有情况下索引只是作为匹配行的非精确向导。但是我们可以使用该索引来寻找边界框与目标对象的边界框相交的对象,并且只在通过该索引找到的对象上做精确的相交测试。如果适用于这种场景,该索引被称为对该操作符是“有损的”。有损索引搜索通过在一行可能满足或者不满足该查询条件时返回一个recheck标志来实现。核心系统将接着在检索到的行上测试原始查询条件来看它是否应该被作为一个合法匹配返回。如果索引被保证能返回所有所需的行外加一些额外的行,这种方法就能有效,因为那些额外的行可以通过执行原始的操作符调用来消除。支持有损搜索的索引方法(当前有 GIN)允许个别操作符类的支持函数设置 recheck 标志,因此这也是一种操作符类的重要特性。
再次考虑在索引中只存储复杂对象(如多边形)的边界框的情况。在这种情况下,把整个多边形存储在索引项中没有很大价值 — 我们也可以只存储一个更简单的box
类型对象。这种情况通过CREATE OPERATOR CLASS
中的STORAGE
选项表示:
CREATE OPERATOR CLASS polygon_ops DEFAULT FOR TYPE polygon USING gin AS ... STORAGE box;
当前,只有 GIN 索引方法支持不同于列数据类型的STORAGE
类型。在 GIN 中,STORAGE
类型标识“key”值的类型,它通常不同于被索引列的类型 — 例如,一个用于整数数组列的操作符类可能具有整数键值。GIN 的支持例程extractValue
和extractQuery
负责从被索引值中抽取键。