前文描述的过程让您能够定义新的类型、新的函数和新的操作符。然而,我们 还不能在新数据类型的列上定义索引。要做到这一点,我们必须为新数据类型 定义一个操作符类。在本节后面,我们将通过一个 例子来说明这个概念:一个用于 B-tree 索引方法的新操作符类,它按绝对值 升序存储和排序复数。
操作符类可以被分组成操作符族,以表明语义兼容的 类之间的关系。当只涉及单一数据类型时,一个操作符类就足够了,因此我们 先关注这种情况,之后再回到操作符族。
操作符类与某个索引访问方法相关联,例如 B-Tree 或 GIN。自定义索引访问方法可以用 CREATE ACCESS METHOD 定义。详情见 Chapter 63。
索引方法的例程对索引方法将要操作的数据类型一无所知。
相反,一个操作
类
标识出索引方法处理特定数据类型所需的操作集合。操作符类之所以这样
命名,是因为它规定的内容之一就是可以与索引一起使用的
WHERE 子句操作符的集合(即可以被转换为索引扫描
条件的操作符)。操作符类还可以指定一些索引方法内部操作所需的
支持函数,这些函数不直接对应于任何可与索引
一起使用的 WHERE 子句操作符。
可以为同一个数据类型和索引方法定义多个操作符类。这样做可以为单个 数据类型定义多套索引语义。例如,B-tree 索引要求为它处理的每种数据 类型定义一个排序顺序。对一个复数数据类型来说,拥有一个按复数绝对值 排序的 B-tree 操作符类、另一个按实部排序的操作符类等等,可能都是有 用的。通常,其中一个操作符类会被认为最常用,并被标记为该数据类型和 索引方法的默认操作符类。
同一个操作符类名
可以用于多个不同的索引方法(例如 B-tree 和哈希索引方法都有名为
int4_ops 的操作符类),但每个这样的类都是独立的
实体,必须分别定义。
与操作符类相关联的操作符通过
“策略号”来标识,策略号用于在操作符类的上下文中标识每个
操作符的语义。
例如,B-tree 对键施加从小到大的严格排序,因此像
“小于”和“大于等于”这样的操作符对 B-tree
而言是有意义的。
由于
PostgreSQL 允许用户定义操作符,
PostgreSQL 无法通过查看操作符的名字
(例如 < 或 >=)判断它是
哪种比较。相反,索引方法定义了一组
“策略”,可以把它们看作广义化的操作符。
每个操作符类针对特定数据类型和索引语义的解释,规定哪个实际操作符
对应于每个策略。
B-tree 索引方法定义了五种策略,如 Table 36.3 所示。
Table 36.3. B-Tree Strategies
| Operation | Strategy Number |
|---|---|
| less than | 1 |
| 小于或等于 | 2 |
| equal | 3 |
| 大于或等于 | 4 |
| greater than | 5 |
哈希索引只支持等值比较,因此只使用一种策略,如 Table 36.4 所示。
Table 36.4. Hash Strategies
| Operation | Strategy Number |
|---|---|
| equal | 1 |
GiST 索引更加灵活:它们根本没有固定的策略集合。相反,每个特定 GiST 操作符类的“一致性”支持例程可以按自己喜欢的方式解释策略 号。举例来说,若干内建 GiST 索引操作符类为二维几何对象建立索引, 提供 Table 36.5 所示的 “R-tree” 策略。其中四个是真正的二维测试(重叠、相同、 包含、被包含);四个只考虑 X 方向;另外四个提供 Y 方向上的相同 测试。
Table 36.5. GiST Two-Dimensional “R-tree” Strategies
| Operation | Strategy Number |
|---|---|
| strictly left of | 1 |
| 不延伸到右边 | 2 |
| overlaps | 3 |
| 不延伸到左边 | 4 |
| strictly right of | 5 |
| same | 6 |
| contains | 7 |
| contained by | 8 |
| 不向上延伸 | 9 |
| strictly below | 10 |
| strictly above | 11 |
| 不向下延伸 | 12 |
SP-GiST 索引在灵活性上与 GiST 索引类似:它们也没有固定的策略集合。 相反,每个操作符类的支持例程根据操作符类的定义来解释策略号。举例 来说,用于点的内建操作符类所使用的策略号如 Table 36.6 所示。
Table 36.6. SP-GiST Point Strategies
| Operation | Strategy Number |
|---|---|
| strictly left of | 1 |
| strictly right of | 5 |
| same | 6 |
| contained by | 8 |
| strictly below | 10 |
| strictly above | 11 |
GIN 索引与 GiST 和 SP-GiST 索引类似,也没有固定的策略集合。相反, 每个操作符类的支持例程根据操作符类的定义来解释策略号。举例来说, 用于数组的内建操作符类所使用的策略号如 Table 36.7 所示。
Table 36.7. GIN Array Strategies
| Operation | Strategy Number |
|---|---|
| overlap | 1 |
| contains | 2 |
| is contained by | 3 |
| equal | 4 |
BRIN 索引与 GiST、SP-GiST 和 GIN 索引类似,也没有固定的策略集合。
相反,每个操作符类的支持例程根据操作符类的定义来解释策略号。举例
来说,内建 Minmax 操作符类所使用的策略号如
Table 36.8 所示。
Table 36.8. BRIN Minmax Strategies
| Operation | Strategy Number |
|---|---|
| less than | 1 |
| 小于或等于 | 2 |
| equal | 3 |
| 大于或等于 | 4 |
| greater than | 5 |
注意,上面列出的所有操作符都返回布尔值。在实践中,被定义为索引方法
搜索操作符的所有操作符都必须返回 boolean 类型,因为它们
必须出现在 WHERE 子句的顶层才能与索引一起使用。
(一些索引访问方法还支持排序操作符,它们通常
不返回布尔值;该特性在
Section 36.16.7 中讨论。)
通常,仅有策略不足以让系统弄清如何使用索引。在实践中,索引方法需要 额外的支持例程才能工作。例如,B-tree 索引方法必须能够比较两个键并 判断其中一个大于、等于还是小于另一个。类似地,哈希索引方法必须能够 为键值计算哈希码。这些操作不对应于 SQL 命令条件中使用的操作符;它们 是索引方法内部使用的管理性例程。
与策略一样,操作符类为给定的数据类型和语义解释指定由哪些具体函数 扮演各个角色。索引方法定义它所需的一组函数,操作符类则通过把它们 分配到索引方法规定的 “支持函数编号”上来指定要使用的正确函数。
此外,一些操作符类允许用户指定控制其行为的参数。每个内建索引访问方法
都有一个可选的 options 支持函数,它定义一组
操作符类特有的参数。
B-tree 需要一个比较支持函数, 并允许操作符类作者按需提供四个额外的支持函数,如 Table 36.9 所示。 对这些支持函数的要求在 Section 65.1.3 中有进一步解释。
Table 36.9. B-Tree Support Functions
| Function | Support Number |
|---|---|
| 比较两个键并返回一个小于零、零或大于零的整数,以指示第一个键是小于、等于还是大于第二个键 | 1 |
| 返回可被 C 调用的排序支持函数的地址(可选) | 2 |
| 将测试值与基准值加/减一个偏移量进行比较,并根据比较结果返回真或假(可选) | 3 |
| 判断使用该操作符类的索引应用 btree 去重优化是否安全(可选) | 4 |
| 定义该操作符类特定的选项(可选) | 5 |
| 返回可被 C 调用的 skip 支持函数的地址(可选) | 6 |
哈希索引需要一个支持函数,并允许操作符类作者按需提供两个额外的支持 函数,如 Table 36.10 所示。
Table 36.10. Hash Support Functions
| Function | Support Number |
|---|---|
| 为一个键计算 32 位哈希值 | 1 |
| 给定一个 64 位盐值,为一个键计算 64 位哈希值;如果盐值为 0,结果的低 32 位必须与函数 1 本应计算出的值相匹配(可选) | 2 |
| 定义该操作符类特定的选项(可选) | 3 |
GiST 索引有十二个支持函数,其中七个是可选的,如 Table 36.11 所示。 (更多信息见 Section 65.2。)
Table 36.11. GiST Support Functions
| Function | Description | Support Number |
|---|---|---|
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 |
SP-GiST 索引有六个支持函数,其中一个是可选的,如 Table 36.12 所示。 (更多信息见 Section 65.3。)
Table 36.12. SP-GiST Support Functions
| Function | Description | Support Number |
|---|---|---|
config | 提供关于该操作符类的基本信息 | 1 |
choose | 决定如何把一个新值插入到内元组中 | 2 |
picksplit | 决定如何划分一组值 | 3 |
inner_consistent | 决定一次查询需要搜索哪些子分区 | 4 |
leaf_consistent | 判断键是否满足查询限定条件 | 5 |
options | 定义该操作符类特定的选项(可选) | 6 |
GIN 索引有七个支持函数,其中四个是可选的,如 Table 36.13 所示。 (更多信息见 Section 65.4。)
Table 36.13. GIN Support Functions
| Function | Description | Support Number |
|---|---|---|
compare | 比较两个键并返回一个小于零、零或大于零的整数,以指示第一个键是小于、等于还是大于第二个键 | 1 |
extractValue | 从要被索引的值中提取键 | 2 |
extractQuery | 从查询条件中提取键 | 3 |
consistent | 判断值是否匹配查询条件(布尔变体)(如果存在支持函数 6 则可选) | 4 |
comparePartial | 比较来自查询的部分键与来自索引的键,并返回一个小于零、零或大于零的整数,以指示 GIN 应该忽略该索引条目、把该条目视为匹配还是停止索引扫描(可选) | 5 |
triConsistent | 判断值是否匹配查询条件(三值变体)(如果存在支持函数 4 则可选) | 6 |
options | 定义该操作符类特定的选项(可选) | 7 |
BRIN 索引有五个基本支持函数,其中一个是可选的,如 Table 36.14 所示。 基本函数的某些版本要求提供额外的支持函数。 (更多信息见 Section 65.5.3。)
Table 36.14. BRIN Support Functions
| Function | Description | Support Number |
|---|---|---|
opcInfo | 返回描述被索引列的汇总数据的内部信息 | 1 |
add_value | 向已有的汇总索引元组添加一个新值 | 2 |
consistent | 判断值是否匹配查询条件 | 3 |
union | 计算两个汇总元组的并集 | 4 |
options | 定义该操作符类特定的选项(可选) | 5 |
与搜索操作符不同,支持函数返回的是特定索引方法所期望的任何数据类型; 例如对 B-tree 的比较函数而言,是一个有符号整数。每个支持函数的参数 数量和类型同样取决于索引方法。对 B-tree 和哈希而言,比较和哈希支持 函数接受的输入数据类型与操作符类所含操作符的输入数据类型相同,但对 大多数 GiST、SP-GiST、GIN 和 BRIN 支持函数而言并非如此。
现在已经看过这些概念了,下面给出之前承诺的创建新操作符类的示例。
(您可以在源码发行版的
src/tutorial/complex.c 和
src/tutorial/complex.sql 中找到这个示例的可用
版本。)
该操作符类封装了按绝对值顺序排序复数的操作符,因此我们
选择名字 complex_abs_ops。首先,我们需要
一组操作符。定义操作符的过程已在 Section 36.14 中讨论过。
对于 B-tree 上的操作符类,我们所需的操作符是:
定义一组相关比较操作符的最不易出错的方法是:先编写 B-tree 比较支持 函数,然后把其他函数写成围绕该支持函数的单行包装。这降低了在边角 情形下得到不一致结果的概率。 按照这一方法,我们先写:
#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);
}
其他四个函数只是把内部函数的结果与零比较的方式不同。
接下来我们把基于这些函数的函数和操作符声明到 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-tree 所需的支持例程。实现它的示例 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-tree 索引了。
我们本可以把操作符条目写得更啰嗦一些,例如:
OPERATOR 1 < (complex, complex) ,
但当操作符与操作符类定义针对的是相同数据类型时,没有必要这样做。
上面的示例假定您想让这个新操作符类成为
complex 数据类型的默认 B-tree 操作符类。
如果不想这样,只需省略 DEFAULT 这个词。
到目前为止,我们隐含地假定操作符类只处理一种数据类型。虽然特定索引 列中确实只能有一种数据类型,但索引把被索引列与不同数据类型的值进行 比较的操作往往是有用的。此外,如果某个操作符类需要用到跨数据类型的 操作符,通常另一个数据类型也有自己的相关操作符类。把这些相关类之间 的联系显式表达出来是有帮助的,因为这可以帮助规划器优化 SQL 查询 (对 B-tree 操作符类尤其如此,因为规划器对如何处理它们有大量内建 知识)。
为了满足这些需求,PostgreSQL 使用了操作符 族的 概念。一个操作符族包含一个或多个操作符类,还可以包含属于整个族而不 属于族内任何单个类的可索引操作符及相应的支持函数。我们说这样的操作 符和函数在族内是“松散”的,与被绑定到特定类中的相对。 通常每个操作符类包含单数据类型的操作符,而跨数据类型的操作符在族内 松散存在。
操作符族中的所有操作符和函数必须具有兼容的语义,兼容性要求由索引 方法设定。因此您可能会奇怪,为什么要把族中的特定子集单独列为操作符 类;确实,对许多目的而言类的划分无关紧要,族才是唯一有意义的分组。 定义操作符类的原因在于:它们规定了支持任何特定索引所需的族的最小 子集。如果存在使用某个操作符类的索引,那么不删除该索引就不能删除 该操作符类 — 但操作符族的其他部分,即其他操作符类和松散 操作符,可以被删除。因此,操作符类应当被指定为包含在特定数据类型的 索引上工作合理所需的最小操作符和函数集合,然后相关但非必需的操作符 可以作为操作符族的松散成员加入。
举例来说,PostgreSQL 有一个内建的
B-tree 操作符族 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) ;
注意,这个定义对操作符策略和支持函数编号做了 “重载”:每个编号在族内出现多次。只要特定编号的每个 实例具有不同的输入数据类型,这就是允许的。两个输入类型都等于某个 操作符类输入类型的实例,就是该操作符类的主要操作符和支持函数, 在大多数情况下应被声明为操作符类的一部分,而不是族的松散成员。
在 B-tree 操作符族中,族内的所有操作符必须按兼容的方式排序,细节 在 Section 65.1.2 中有明确规定。 族内的每个操作符都必须有一个与其具有相同两个输入数据类型的支持 函数。建议族是完备的,即对每种数据类型组合,包含所有操作符。 每个操作符类应只包含针对其数据类型的非跨类型操作符和支持函数。
要构建多数据类型的哈希操作符族,必须为族支持的每种数据类型创建 兼容的哈希支持函数。这里的兼容指的是:对族内相等操作符认为相等的 任意两个值,函数保证返回相同的哈希码,即使这两个值的类型不同。 当类型的物理表示不同时这通常很难做到,但在某些情况下可以实现。 此外,通过隐式或二进制强转把值从族中表示的一种数据类型转换为族中 表示的另一种数据类型时,不得改变计算出的哈希值。 注意,每种数据类型只有一个支持函数,而不是每个相等操作符一个。 建议族是完备的,即为每种数据类型组合提供相等操作符。 每个操作符类应只包含针对其数据类型的非跨类型相等操作符和支持函数。
GiST、SP-GiST 和 GIN 索引没有跨数据类型操作的显式概念。所支持的 操作符集合就是给定操作符类的主要支持函数所能处理的那些。
在 BRIN 中,要求取决于提供操作符类的框架。对基于
minmax 的操作符类,所需的行为与 B-tree 操作符族
相同:族内的所有操作符必须按兼容的方式排序,且强转不得改变相关的
排序顺序。
在 PostgreSQL 8.3 之前,没有操作符族的 概念,因此任何打算与索引一起使用的跨数据类型操作符都必须直接绑定 到索引的操作符类中。这种方法仍然有效,但已被弃用,因为它使索引的 依赖范围过宽,而且当两种数据类型的操作符位于同一操作符族中时, 规划器能更有效地处理跨数据类型的比较。
PostgreSQL 利用操作符类来推断操作符的 属性,用途远不止判断它们能否与索引一起使用。因此,即使您无意对 数据类型的任何列建立索引,可能也值得创建操作符类。
特别地,ORDER BY 和
DISTINCT 等 SQL 特性需要对值进行比较和排序。
为了在用户定义的数据类型上实现这些特性,
PostgreSQL 会查找该数据类型的默认
B-tree 操作符类。这个操作符类的“equals”成员定义了系统
对 GROUP BY 和 DISTINCT 的值
相等概念,而该操作符类施加的排序顺序定义了默认的
ORDER BY 排序。
如果数据类型没有默认的 B-tree 操作符类,系统会查找默认的哈希 操作符类。但由于那种操作符类只提供相等性,它只能支持分组, 无法支持排序。
当数据类型没有默认操作符类时,如果对该数据类型使用这些 SQL 特性, 您会得到类似“could not identify an ordering operator” 的错误。
在 PostgreSQL 7.4 之前的版本中,排序和
分组操作会隐式使用名为
=、< 和 >
的操作符。依赖默认操作符类的新行为避免了必须对具有特定名字的
操作符的行为做任何假设。
通过在 USING 选项中指定类的小于操作符,可以按
非默认的 B-tree 操作符类排序,例如
SELECT * FROM mytable ORDER BY somecol USING ~<~;
或者,在 USING 中指定类的大于操作符会选择降序
排序。
对用户定义类型的数组进行比较也依赖于该类型默认 B-tree 操作符类 定义的语义。如果没有默认的 B-tree 操作符类,但有默认的哈希 操作符类,那么支持数组相等,但不支持排序比较。
另一个需要更多数据类型特定知识的 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-tree 操作符类
提供的比较操作符 — 但加法和减法操作符不属于操作符类,那么
应该使用哪些呢?把这一选择硬编码是不可取的,因为不同的排序顺序
(不同的 B-tree 操作符类)可能需要不同的行为。因此,B-tree 操作符
类可以指定一个in_range 支持函数,封装对其
排序顺序有意义的加法和减法行为。
它甚至可以提供多个 in_range 支持函数,以备有不止一种数据类型适合
用作 RANGE 子句中的偏移。
如果与窗口的 ORDER
BY 子句关联的 B-tree 操作符类没有匹配的 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 索引可以高效地做到这一点,因为
<-> 是排序操作符。
搜索操作符必须返回布尔结果,而排序操作符通常返回其他类型,例如
距离用的 float 或 numeric。
这个类型通常与被索引的数据类型不同。
为了避免对不同数据类型的行为做硬编码假设,排序操作符的定义被要求
指定一个 B-tree 操作符族,以规定结果数据类型的排序顺序。
如前文所述,B-tree 操作符族定义了
PostgreSQL 的排序概念,因此这是一种自然
的表示。由于点上的 <->
操作符返回 float8,它可以在操作符类创建命令中这样
指定:
OPERATOR 15 <-> (point, point) FOR ORDER BY float_ops
其中 float_ops 是包含 float8 操作的
内建操作符族。这一声明表示该索引能够按 <->
操作符值的递增顺序返回行。
操作符类还有两个我们尚未讨论的特殊特性,主要因为它们对最常用的索引 方法没什么用处。
通常,把一个操作符声明为操作符类(或族)的成员意味着索引方法可以
精确取回满足使用该操作符的 WHERE 条件的行的
集合。例如:
SELECT * FROM table WHERE integer_column < 4;
可以由 integer 列上的 B-tree 索引精确满足。
但在某些情况下,索引作为匹配行的非精确指引也是有用的。
例如,如果 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 类型。
使用 STORAGE 时,GiST 的
compress 和 decompress
支持例程必须处理数据类型转换。
当存储类型不同时,SP-GiST 同样需要 compress
支持函数来完成到存储类型的转换;如果 SP-GiST 操作符类还支持取回
数据,反向转换必须由 consistent 函数处理。
在 GIN 中,STORAGE 类型标识的是
“键”值的类型,它通常与被索引列的类型不同 — 例如,
用于整数数组列的操作符类的键可能只是整数。
GIN 的 extractValue 和
extractQuery 支持例程负责从被索引值中提取
键。
BRIN 与 GIN 类似:STORAGE 类型标识所存储汇总值
的类型,操作符类的支持过程负责正确解释这些汇总值。