到目前为止所描述的过程使我们能够定义新类型、新函数以及新操作符。然而,我们还不能在一种新数据类型的列上定义索引。为此,必须为该新数据类型定义一个操作符类。本节稍后将用一个示例说明这一概念:为 B-树索引方法定义一个新的操作符类,以便按绝对值升序存储和排序复数。
操作符类可以分组成操作符族,以展示语义兼容的类之间的关系。只涉及单一数据类型时,一个操作符类就已足够,因此我们先关注这种情况,然后再回到操作符族。
操作符类与索引访问方法相关联,例如 B-树 或 GIN。 自定义索引访问方法可通过 CREATE ACCESS METHOD 定义。 详见 Chapter 62。
索引方法的例程并不直接知道它将处理哪些数据类型。相反,一个操作符类标识了索引方法在处理特定数据类型时需要使用的那组操作。之所以称为操作符类,是因为它指定的一项内容就是可与索引一起使用的 WHERE 子句操作符集合(也就是能被转换成索引扫描条件的操作符)。操作符类还可以指定索引方法内部操作所需的一些支持函数,但这些函数并不直接对应任何可与索引一起使用的 WHERE 子句操作符。
可以为同一种数据类型和索引方法定义多个操作符类。这样就能为一种数据类型定义多套索引语义。例如,一个 B-树索引要求为其处理的每一种数据类型定义一种排序顺序。对于复数数据类型,也许既需要一个按复数绝对值排序的 B-树操作符类,也需要另一个按实部排序的操作符类,等等。通常,其中一个操作符类会被视为最常用,并标记为该数据类型在该索引方法上的默认操作符类。
同一个操作符类名可以用于多个不同的索引方法(例如,B-树和哈希索引方法都有名为 int4_ops 的操作符类),但每一个这样的类都是独立实体,必须分别定义。
与操作符类关联的操作符通过“策略号”来标识,用以表示每个操作符在其操作符类上下文中的语义。例如,B-树对键施加了严格的从小到大的顺序,因此像“小于”和“大于等于”这样的操作符,对 B-树来说就很重要。由于 PostgreSQL 允许用户定义操作符,PostgreSQL 不能仅凭操作符名称(例如 < 或 >=)就判断它是哪一类比较。取而代之的是,索引方法定义了一组“策略”,可以把它们看成是广义的操作符。每个操作符类都会说明,对于某种特定数据类型和某种索引语义解释,每一种策略分别对应哪个实际操作符。
B-树索引方法定义了五种策略,如Table 36.3所示。
Table 36.3. B-树策略
| 操作 | 策略号 |
|---|---|
| 小于 | 1 |
| 小于等于 | 2 |
| 等于 | 3 |
| 大于等于 | 4 |
| 大于 | 5 |
哈希索引只支持等值比较,因此它们只使用一种策略,如Table 36.4所示。
Table 36.4. 哈希策略
| 操作 | 策略号 |
|---|---|
| 等于 | 1 |
GiST 索引更加灵活:它们根本没有固定的策略集合。相反,每个特定 GiST 操作符类中负责“一致性”检查的支持例程会按自己的方式解释策略号。举例来说,一些内置的 GiST 索引操作符类会为二维几何对象建立索引,并提供“R 树”策略,如Table 36.5所示。其中四个是真正的二维测试(重叠、相同、包含、被包含),四个只考虑 X 方向,另外四个则在 Y 方向上提供相同测试。
Table 36.5. GiST 二维“R 树”策略
| 操作 | 策略号 |
|---|---|
| 严格位于左侧 | 1 |
| 不延伸到右侧 | 2 |
| 重叠 | 3 |
| 不延伸到左侧 | 4 |
| 严格位于右侧 | 5 |
| 相同 | 6 |
| 包含 | 7 |
| 被包含 | 8 |
| 不延伸到上方 | 9 |
| 严格位于下方 | 10 |
| 严格位于上方 | 11 |
| 不延伸到下方 | 12 |
SP-GiST 索引在灵活性方面与 GiST 索引类似:它们也没有固定的策略集合。相反,每个操作符类的支持例程会根据该操作符类的定义解释策略号。举例来说,内置点操作符类所使用的策略号如Table 36.6所示。
Table 36.6. SP-GiST 点策略
| 操作 | 策略号 |
|---|---|
| 严格位于左侧 | 1 |
| 严格位于右侧 | 5 |
| 相同 | 6 |
| 被包含 | 8 |
| 严格位于下方 | 10 |
| 严格位于上方 | 11 |
GIN 索引与 GiST 和 SP-GiST 索引类似,也没有固定的策略集合。相反,每个操作符类的支持例程会根据该操作符类的定义解释策略号。举例来说,内置数组操作符类所使用的策略号如Table 36.7所示。
Table 36.7. GIN 数组策略
| 操作 | 策略号 |
|---|---|
| 重叠 | 1 |
| 包含 | 2 |
| 被包含 | 3 |
| 等于 | 4 |
BRIN 索引也与 GiST、SP-GiST 和 GIN 索引一样,没有固定的策略集合。每个操作符类的支持函数都会根据该操作符类的定义解释策略号。举例来说,内置 Minmax 操作符类所使用的策略号如Table 36.8所示。
Table 36.8. BRIN Minmax 策略
| 操作 | 策略号 |
|---|---|
| 小于 | 1 |
| 小于等于 | 2 |
| 等于 | 3 |
| 大于等于 | 4 |
| 大于 | 5 |
注意,上面列出的所有操作符都返回布尔值。实际上,所有被定义为索引方法搜索操作符的操作符都必须返回 boolean,因为要与索引配合使用,它们必须出现在 WHERE 子句的顶层。(某些索引访问方法还支持排序操作符,这类操作符通常不返回布尔值;该特性见Section 36.16.7。)
仅靠策略信息通常不足以让系统知道如何使用索引。实际上,索引方法还需要额外的支持例程才能工作。例如,B-树索引方法必须能够比较两个键,并判断其中一个是大于、等于还是小于另一个。类似地,哈希索引方法必须能够为键值计算哈希码。这些操作并不对应 SQL 命令条件中使用的操作符;它们是索引方法内部使用的管理例程。
与策略一样,操作符类会标识对于给定的数据类型和语义解释,应由哪些具体函数承担这些角色。索引方法定义它需要的函数集合,而操作符类则会通过为函数分配由索引方法规定的“支持函数号”来标识正确的函数。
此外,有些操作符类允许用户指定控制其行为的参数。每种内置索引访问方法都有一个可选的 options 支持函数,用来定义一组该操作符类特有的参数。
如Table 36.9所示,B-树要求一个比较支持函数,并允许操作符类作者按需再提供四个支持函数。这些支持函数的要求会在Section 3.3中进一步解释。
Table 36.9. B-树支持函数
| 函数 | 支持号 |
|---|---|
| 比较两个键,并返回一个小于零、等于零或大于零的整数,用以表示第一个键是小于、等于还是大于第二个键 | 1 |
| 返回可由 C 调用的排序支持函数地址(可选) | 2 |
| 将测试值与基准值加上或减去某个偏移量后的结果进行比较,并根据比较结果返回真或假(可选) | 3 |
| 确定对使用该操作符类的索引应用 B-树去重优化是否安全(可选) | 4 |
| 定义该操作符类特有的选项(可选) | 5 |
| 返回可由 C 调用的 skip 支持函数地址(可选) | 6 |
如Table 36.10所示,哈希索引要求一个支持函数,并允许操作符类作者按需再提供两个支持函数。
Table 36.10. 哈希支持函数
| 函数 | 支持号 |
|---|---|
| 为一个键计算 32 位哈希值 | 1 |
| 给定一个 64 位 salt,计算一个键的 64 位哈希值;如果 salt 为 0,结果的低 32 位必须与函数 1 计算出的值一致(可选) | 2 |
| 定义该操作符类特有的选项(可选) | 3 |
GiST 索引有十二个支持函数,其中七个是可选的,如Table 36.11所示。 (详见Section 4。)
Table 36.11. GiST 支持函数
| 函数 | 描述 | 支持号 |
|---|---|---|
consistent |
确定键是否满足查询条件 | 1 |
union |
计算一组键的并集 | 2 |
compress |
计算将被索引的键或值的压缩表示(可选) | 3 |
decompress |
计算压缩键的解压表示(可选) | 4 |
penalty |
计算把新键插入具有给定子树键的子树时的罚值 | 5 |
picksplit |
确定页面中的哪些项要移到新页面,并计算结果页面的并集键 | 6 |
same |
比较两个键,并在它们相等时返回真 | 7 |
distance |
确定键到查询值的距离(可选) | 8 |
fetch |
为仅索引扫描计算压缩键的原始表示(可选) | 9 |
options |
定义该操作符类特有的选项 (可选) | 10 |
sortsupport |
提供一个用于快速索引构建的排序比较器 (可选) | 11 |
translate_cmptype |
把比较类型转换为该操作符类使用的策略号(可选) | 12 |
如Table 36.12所示,SP-GiST 索引有六个支持函数,其中一个是可选的。(详见Section 5。)
Table 36.12. SP-GiST 支持函数
| 函数 | 描述 | 支持号 |
|---|---|---|
config |
提供有关该操作符类的基本信息 | 1 |
choose |
确定如何把一个新值插入内部元组 | 2 |
picksplit |
确定如何划分一组值 | 3 |
inner_consistent |
确定对某个查询需要搜索哪些子分区 | 4 |
leaf_consistent |
确定键是否满足查询条件 | 5 |
options |
定义该操作符类特有的选项 (可选) | 6 |
如Table 36.13所示,GIN 索引有七个支持函数,其中四个是可选的(详见Section 6)。
Table 36.13. GIN 支持函数
| 函数 | 描述 | 支持号 |
|---|---|---|
compare |
比较两个键,并返回一个小于零、等于零或大于零的整数,以表示第一个键是小于、等于还是大于第二个键 | 1 |
extractValue |
从待索引值中提取键 | 2 |
extractQuery |
从查询条件中提取键 | 3 |
consistent |
确定值是否匹配查询条件(布尔变体;如果支持函数 6 存在则可选) | 4 |
comparePartial |
比较查询中的部分键与索引中的键,并返回一个小于零、等于零或大于零的整数,用以指示 GIN 应忽略该索引项、将该项视为匹配,还是停止索引扫描(可选) | 5 |
triConsistent |
确定值是否匹配查询条件(三值变体;如果支持函数 4 存在则可选) | 6 |
options |
定义该操作符类特有的选项(可选) | 7 |
BRIN 索引有五个基本支持函数,其中一个是可选的,如Table 36.14所示。 某些版本的基本函数还要求提供额外的支持函数。(详见Section 7.3。)
Table 36.14. BRIN 支持函数
| 函数 | 描述 | 支持号 |
|---|---|---|
opcInfo |
返回描述被索引列摘要数据的内部信息 | 1 |
add_value |
向一个现有的摘要索引元组增加一个新值 | 2 |
consistent |
确定值是否匹配查询条件 | 3 |
union |
计算两个摘要元组的并集 | 4 |
options |
定义该操作符类特有的选项(可选) | 5 |
与搜索操作符不同,支持函数返回的是特定索引方法所期望的数据类型;例如,对 B-树的比较函数来说,就是一个有符号整数。每个支持函数的参数个数和类型也同样取决于索引方法。对于 B-树和哈希,比较支持函数和哈希支持函数接受的输入数据类型,与操作符类中包含的操作符相同;但对大多数 GiST、SP-GiST、GIN 和 BRIN 支持函数来说并非如此。
现在我们已经了解了这些基本思想,下面给出先前承诺的创建新操作符类示例。(这个可运行示例位于源码发布包中的src/tutorial/complex.c和src/tutorial/complex.sql。)该操作符类封装了一组按绝对值顺序对复数排序的操作符,因此我们把它命名为complex_abs_ops。首先,我们需要一组操作符。定义操作符的过程已经在Section 36.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 选用其他操作符名。
尽管 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使用操作符族这一概念。一个操作符族包含一个或多个操作符类,还可以包含属于整个族、但不属于族中任何单一类的可索引操作符及其相应的支持函数。我们称这样的操作符和函数在该族中是“松散”的,而不是绑定在某个特定类中。通常,每个操作符类只包含单一数据类型的操作符,而跨数据类型操作符则作为操作符族中的松散成员存在。
一个操作符族中的所有操作符和函数都必须具有兼容的语义,而兼容性的要求由索引方法设定。因此,你也许会疑惑,为什么还要把该族的某些子集单独划成操作符类;事实上,对很多用途而言,类的划分并不重要,真正有意义的分组只有操作符族。之所以定义操作符类,是因为它们规定了支持特定索引所需的操作符族内容。如果某个索引使用了某个操作符类,那么在不删除该索引的情况下就不能删除该操作符类 — 但操作符族中的其他部分,也就是其他操作符类和松散操作符,则可以被删除。因此,一个操作符类应当只包含在特定数据类型上支持索引所合理需要的最小操作符和函数集合,而那些相关但非必需的操作符,则可以作为操作符族的松散成员加入。
例如,PostgreSQL内置了一个 B-树操作符族 integer_ops,其中包含操作符类 int8_ops、int4_ops 和 int2_ops,它们分别用于 bigint(int8)、integer(int4)和 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) , FUNCTION 2 btint8sortsupport(internal) , FUNCTION 3 in_range(int8, int8, int8, boolean, boolean) , FUNCTION 4 btequalimage(oid) , FUNCTION 6 btint8skipsupport(internal) ; 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) , FUNCTION 2 btint4sortsupport(internal) , FUNCTION 3 in_range(int4, int4, int4, boolean, boolean) , FUNCTION 4 btequalimage(oid) , FUNCTION 6 btint4skipsupport(internal) ; 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) , FUNCTION 2 btint2sortsupport(internal) , FUNCTION 3 in_range(int2, int2, int2, boolean, boolean) , FUNCTION 4 btequalimage(oid) , FUNCTION 6 btint2skipsupport(internal) ; 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) , -- cross-type in_range functions 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) ;
注意,这个定义对操作符策略号和支持函数号进行了“重载”:每个编号在该族内都会出现多次。只要某个编号的每次出现都具有不同的输入数据类型,这就是允许的。那些两个输入类型都等于某个操作符类输入类型的实例,就是该操作符类的主要操作符和支持函数;在大多数情况下,它们应当被声明为该操作符类的一部分,而不是该族的松散成员。
如Section 3.2所详述,在一个 B-树操作符族中,该族中的所有操作符都必须以兼容的方式排序。对族中的每一个操作符,都必须有一个具有相同两个输入数据类型的支持函数。建议让操作符族保持完整,也就是说,对每一种数据类型组合都应包含全部操作符。每个操作符类只应包含其数据类型对应的非跨数据类型操作符和支持函数。
要构建一个多数据类型的哈希操作符族,必须为该族支持的每一种数据类型创建相互兼容的哈希支持函数。这里的兼容性是指:对任意两个被该族中的等值操作符视为相等的值,这些函数都保证返回相同的哈希码,即使这两个值属于不同类型也是如此。当这些类型具有不同的物理表示时,这通常难以实现,但在某些情况下可以做到。此外,将该操作符族中一种数据类型的值通过隐式或二进制可强制类型转换转换成该族中另一种数据类型时,不应改变所计算出的哈希值。注意,每种数据类型只有一个支持函数,而不是每个等值操作符一个。建议让操作符族保持完整,也就是说,对每一种数据类型组合都提供一个等值操作符。每个操作符类只应包含其数据类型对应的非跨数据类型等值操作符和支持函数。
GiST、SP-GiST 和 GIN 索引没有任何显式的跨数据类型操作概念。它们所支持的操作符集合,就是给定操作符类的主要支持函数所能处理的那些操作符。
在 BRIN 中,要求取决于提供操作符类的框架。对于基于 minmax 的操作符类,所要求的行为与 B-树操作符族相同:族中的所有操作符都必须以兼容的方式排序,并且类型转换不能改变相关的排序顺序。
在 PostgreSQL 8.3 之前,并没有操作符族这一概念,因此任何打算与索引一起使用的跨数据类型操作符都必须直接绑定到该索引的操作符类中。虽然这种做法仍然有效,但已经废弃,因为它会使索引的依赖关系过于宽泛,而且当两种数据类型都在同一操作符族中拥有操作符时,规划器能更有效地处理跨数据类型比较。
PostgreSQL利用操作符类来从多方面推断操作符的属性,而不仅仅是判断它们能否用于索引。因此,即便你并不打算为自己的数据类型列建立索引,也可能会想创建操作符类。
特别地,ORDER BY和DISTINCT等 SQL 特性要求对值的比较和排序。为了在用户定义的数据类型上实现这些特性,PostgreSQL会为数据类型查找默认 B-树操作符类。这个操作符类的“equals”成员定义了用于GROUP BY和DISTINCT的值的等值概念,而该操作符类施加的排序顺序定义了默认的ORDER BY顺序。
如果一种数据类型没有默认的 B-树操作符类,系统就会查找默认的哈希操作符类。但由于这类操作符类只提供等值语义,因此它只能支持分组,不能支持排序。
如果某种数据类型没有默认操作符类,而你又试图将这些 SQL 特性用于该数据类型,就会得到类似“无法识别排序操作符”这样的错误。
在版本 7.4 以前的PostgreSQL中,排序和分组操作将隐式地使用名为=、<以及>的操作符。新的依赖于默认操作符类的行为避免了对具有特定名字的操作符行为作出任何假设。
可以通过在 USING 选项中指定某个非默认 B-树操作符类的小于操作符,按该操作符类进行排序,例如
SELECT * FROM mytable ORDER BY somecol USING ~<~;
或者,在 USING 中指定该操作符类的大于操作符,就会选择降序排序。
用户定义类型的数组比较也依赖于该类型默认 B-树操作符类所定义的语义。如果没有默认 B-树操作符类,但有默认的哈希操作符类,则支持数组相等比较,但不支持顺序比较。
另一种需要更多数据类型相关知识的 SQL 特性,是窗口函数中的 RANGE offset PRECEDING/FOLLOWING 框架选项(见Section 4.2.8)。对于下面这样的查询
SELECT sum(x) OVER (ORDER BY x RANGE BETWEEN 5 PRECEDING AND 10 FOLLOWING) FROM mytable;
仅仅知道如何按 x 排序还不够;数据库还必须理解如何对当前行的 x 值“减 5”或“加 10”,以标识当前窗口帧的边界。把得到的边界与其他行的 x 值进行比较是可行的,因为可以使用定义 ORDER BY 顺序的 B-树操作符类所提供的比较操作符 — 但加法和减法操作符并不是该操作符类的一部分,那么该用哪些操作符呢?把这种选择硬编码下来并不合适,因为不同的排序顺序(不同的 B-树操作符类)可能需要不同的行为。因此,B-树操作符类可以指定一个in_range支持函数,用来封装对其排序顺序有意义的加减行为。它甚至可以提供多个 in_range 支持函数,以防有多种数据类型都适合作为 RANGE 子句中的偏移量。如果与窗口 ORDER BY 子句关联的 B-树操作符类没有匹配的 in_range 支持函数,则不支持 RANGE offset PRECEDING/FOLLOWING 选项。
另一个重要点在于,出现在哈希操作符族中的等值操作符,都是哈希连接、哈希聚合以及相关优化的候选对象。这里哈希操作符族至关重要,因为它标识了应当使用的哈希函数。
有些索引访问方法(目前只有 GiST 和 SP-GiST)支持排序操作符这一概念。到目前为止,我们讨论的都是搜索操作符。对搜索操作符而言,可以搜索索引以找出所有满足 WHERE indexed_column operator constant 的行。注意,返回匹配行的顺序并没有任何保证。相反,排序操作符并不限制可返回的行集合,而是决定这些行的顺序。对排序操作符而言,可以扫描索引以按如下表达式所表示的顺序返回行: ORDER BY indexed_column operator constant 之所以这样定义排序操作符,是因为如果该操作符能度量距离,它就能支持最近邻搜索。例如,像下面这样的查询
SELECT * FROM places ORDER BY location <-> point '(101,456)' LIMIT 10;
会找出离给定目标点最近的十个位置。对 location 列建立的 GiST 索引可以高效地完成这个查询,因为 <-> 是一个排序操作符。
搜索操作符必须返回布尔结果,而排序操作符通常返回其他类型的结果,例如表示距离的浮点数或数值。这种类型通常不同于被索引的数据类型。为了避免对不同数据类型行为作硬编码假设,在定义排序操作符时,必须指定一个 B-树操作符族,用来说明结果数据类型的排序顺序。正如上一节所述,B-树操作符族定义了 PostgreSQL 的顺序概念,因此这是一种自然的表示方式。由于点的 <-> 操作符返回 float8,因此可以在创建操作符类时这样指定它:
OPERATOR 15 <-> (point, point) FOR ORDER BY float_ops
其中 float_ops 是包含针对 float8 的操作的内置操作符族。这个声明表明,该索引能够按 <-> 操作符值递增的顺序返回行。
还有两个操作符类的特殊特性我们尚未讨论,主要是因为它们对最常用的索引方法并不太有用。
通常,把一个操作符声明为某个操作符类(或操作符族)的成员,意味着该索引方法能够利用该操作符准确检索出满足 WHERE 条件的那组行。例如:
SELECT * FROM table WHERE integer_column < 4;
这个查询可以由整数列上的 B-树索引精确满足。但也有一些情况下,索引只能作为匹配行的不精确指引。例如,如果某个 GiST 索引只存储几何对象的边界框,那么它就无法精确满足测试非矩形对象(如多边形)之间重叠的 WHERE 条件。不过,我们仍然可以利用该索引找出边界框与目标对象边界框重叠的对象,然后只对索引找到的那些对象执行精确的重叠测试。如果适用于这种场景,就称该索引对这个操作符是“有损的”。有损索引搜索的实现方式是:当某一行可能满足、也可能不满足查询条件时,由索引方法返回一个recheck标志。随后,核心系统会在取回的行上重新测试原始查询条件,以判断它是否应当作为合法匹配返回。只要索引能保证返回所有必需的行,外加可能存在的一些额外行,这种方法就是有效的,因为这些额外行可以通过执行原始操作符调用来剔除。支持有损搜索的索引方法(目前有 GiST、SP-GiST 和 GIN)允许个别操作符类的支持函数设置 recheck 标志,因此这本质上也是一种操作符类特性。
再考虑一下那种只在索引中存储复杂对象(如多边形)边界框的情况。在这种情况下,把整个多边形存储在索引项里意义不大 — 我们完全可以只存储一个更简单的 box 类型对象。这种情况可以在 CREATE OPERATOR CLASS 中通过 STORAGE 选项表示出来:
CREATE OPERATOR CLASS polygon_ops
DEFAULT FOR TYPE polygon USING gist AS
...
STORAGE box;
目前,只有 GiST、SP-GiST、GIN 和 BRIN 索引方法支持与列数据类型不同的 STORAGE 类型。GiST 的 compress 和 decompress 支持例程在使用 STORAGE 时必须处理数据类型转换。SP-GiST 同样需要 compress 支持函数在存储类型不同时完成转换;如果某个 SP-GiST 操作符类还支持取回数据,那么反向转换必须由 consistent 函数处理。在 GIN 中,STORAGE 类型标识“key”值的类型,它通常不同于被索引列的类型 — 例如,整数数组列上的某个操作符类,其键值可能只是整数。GIN 的 extractValue 和 extractQuery 支持例程负责从被索引值中提取键。BRIN 与 GIN 类似:STORAGE 类型标识被存储的摘要值类型,而操作符类的支持过程负责正确解释这些摘要值。
如果您发现文档中有不正确的内容、与您使用特定功能的经验不符或需要进一步说明,请使用此表单来报告文档问题。