pgsql.cc 提供对 postgresql.org 官网内容的中文翻译,由 Pigsty 团队维护。
到目前为止所描述的过程使我们能够定义新类型、新函数以及新操作符。然而,我们还不能在一个新类型或其操作符上定义一个二级索引(例如 B-tree、R-tree 或哈希访问方法)。
回头看Postgres 的主要系统目录。右半部分显示了我们必须修改的目录,以便告诉 Postgres 如何在索引中使用用户定义的类型和/或用户定义的操作符 (即 pg_am, pg_amop, pg_amproc, pg_operator 和 pg_opclass)。 遗憾的是,没有一条简单的命令可以完成这件事。我们将通过一个贯穿始终的示例来演示如何修改这些目录:为 B-tree 访问方法定义一个新的操作符类,以便按绝对值升序存储和排序复数。
pg_am 类为每个用户定义的访问方法保存一个实例。堆访问方法的支持内置于 Postgres 中,但每个其他访问方法都在这里描述。其模式是
表 37.1. 索引模式
| 属性 | 描述 |
|---|---|
| amname | 访问方法的名称 |
| amowner | 属主在 pg_user 中实例的对象 id |
| amkind | 当前未使用,但保留为占位符并置为 'o' |
| amstrategies | 该访问方法的策略数目(见下文) |
| amsupport | 该访问方法的支持例程数目(见下文) |
| amgettuple | |
| aminsert | |
| ... | 访问方法接口例程的过程标识符。例如,打开、关闭访问方法以及从中获取实例的 regproc id 就出现在这里。 |
pg_am 中该实例的对象 ID 被用作许多其他类中的外键。你不需要向这个类添加新实例;你所关心的只是你想扩展的访问方法实例的对象 ID:
SELECT oid FROM pg_am WHERE amname = 'btree';
+----+
|oid |
+----+
|403 |
+----+
我们稍后会在 WHERE 子句中使用那条 SELECT。
amstrategies 属性的存在是为了标准化跨数据类型的比较。例如,B-tree 对键施加了严格的从小到大的顺序。由于 Postgres 允许用户定义操作符,Postgres 不能仅凭操作符名称(例如 ">" 或 "<")就判断它是哪一类比较。事实上,某些访问方法根本不施加任何排序。例如,R-tree 表达一种矩形包含关系,而哈希数据结构只表达基于哈希函数值的按位相似性。Postgres 需要某种一致的方式,来接受你查询中的条件、查看操作符,然后判断是否存在可用的索引。这蕴含着 Postgres 需要知道,例如,"<=" 和 ">" 操作符划分一个 B-tree。Postgres 使用策略来表达操作符与它们可用于扫描索引的方式之间的这些关系。
定义一组新的策略超出了本讨论的范围,但我们将解释 B-tree 策略如何工作,因为你要添加一个新的操作符类就需要了解它。在 pg_am 类中,amstrategies 属性是该访问方法所定义的策略数目。对 B-tree 来说,这个数是 5。这些策略对应于
表 37.2. B-tree 策略
| 操作 | 索引号 |
|---|---|
| 小于 | 1 |
| 小于等于 | 2 |
| 等于 | 3 |
| 大于等于 | 4 |
| 大于 | 5 |
这个思路是:你需要把与上述比较对应的过程添加到 pg_amop 关系中(见下文)。访问方法代码可以使用这些策略号(不管数据类型是什么)来弄清如何划分 B-tree、计算选择性等等。现在还不用担心添加 过程的细节;只需理解:对 int2, int4, oid, 以及 B-tree 可以操作的每个其他数据类型,都必须存在一组这样的过程。
有时,仅靠策略信息不足以让系统弄清如何使用索引。某些访问方法还需要其他支持例程才能工作。例如,B-tree 访问方法必须能够比较两个键,并判断其中一个是大于、等于还是小于另一个。类似地,R-tree 访问方法必须能够计算矩形的交集、并集和大小。这些操作并不对应 SQL 查询中的用户条件;它们是访问方法内部使用的管理例程。
为了在所有 Postgres 访问方法之间一致地管理多样的支持例程,pg_am 包含一个名为 amsupport 的属性。该列记录一个访问方法所使用的支持例程数目。对 B-tree 来说,这个数是一——接受两个键并根据第一个键小于、等于还是大于第二个键而返回 -1、0 或 +1 的例程。
严格地说,该例程可以返回一个负数(< 0)、0 或一个非零正数(> 0)。
pg_am 中的 amstrategies 项只是所论访问方法定义的策略数目。小于、小于等于等的过程并不出现在 pg_am 中。类似地,amsupport 只是该访问方法所需的支持例程数目。实际的例程列在别处。
下一个我们关心的类是 pg_opclass。这个类的存在只是为了把一个名称和默认类型与一个 oid 相关联。在 pg_amop 中,每个 B-tree 操作符类都有上文的第 1 到第 5 号一组过程。一些现有的操作符类是 int2_ops, int4_ops, 和 oid_ops。你需要把带有你的 操作符类名(例如 complex_abs_ops)的一个实例添加到 pg_opclass。这一实例的 oid 是其他类中的外键。
INSERT INTO pg_opclass (opcname, opcdeftype)
SELECT 'complex_abs_ops', oid FROM pg_type WHERE typname = 'complex_abs';
SELECT oid, opcname, opcdeftype
FROM pg_opclass
WHERE opcname = 'complex_abs_ops';
+------+-----------------+------------+
|oid | opcname | opcdeftype |
+------+-----------------+------------+
|17314 | complex_abs_ops | 29058 |
+------+-----------------+------------+
注意,你的 pg_opclass 实例的 oid 会不同!不过不必担心。稍后我们会像这里获取类型的 oid 一样从系统中取得这个数。
现在我们有了一个访问方法和一个操作符类。我们还需要一组操作符;定义操作符的过程已在本手册前面讨论过。对于 Btree 上的 complex_abs_ops 操作符类,我们需要的操作符是:
absolute value less-than
absolute value less-than-or-equal
absolute value equal
absolute value greater-than-or-equal
absolute value greater-than
假设实现所定义函数的代码存储在文件 PGROOT/src/tutorial/complex.c 中
代码的一部分看起来像这样:(注意,在余下的示例中我们只展示相等操作符。其他四个操作符非常类似。细节请参阅 complex.c 或 complex.source。)
#define Mag(c) ((c)->x*(c)->x + (c)->y*(c)->y)
bool
complex_abs_eq(Complex *a, Complex *b)
{
double amag = Mag(a), bmag = Mag(b);
return (amag==bmag);
}
下面有几点重要的事情正在发生。
首先,注意这里正在为 int4 定义小于、小于等于、等于、 大于等于和大于操作符。所有这些操作符都已以 <、<=、=、>= 和 > 的名称为 int4 定义过。当然,新的操作符行为不同。为了确保 Postgres 使用这些新操作符而不是旧操作符,它们的命名必须与旧操作符不同。这是一个要点:你可以在 Postgres 中重载操作符,但仅当该操作符尚未为这些参数类型定义时才行。也就是说,如果你已经为 (int4, int4) 定义了 <,就不能再定义一次。 Postgres 在你定义操作符时不检查这一点,所以要小心。为了避免这个问题,将为这些操作符使用奇怪的名字。如果你弄错了,访问方法在你尝试做扫描时很可能会崩溃。
另一个要点是所有操作符函数都返回布尔值。访问方法依赖这一事实。(另一方面,支持函数返回的是特定访问方法所期望的类型——在本例中就是一个有符号整数。)文件中的最后一个例程是我们在讨论 pg_am 类的 amsupport 属性时提到的"支持例程"。我们稍后会用到它。现在先忽略它。
CREATE FUNCTION complex_abs_eq(complex_abs, complex_abs)
RETURNS bool
AS 'PGROOT/tutorial/obj/complex.so'
LANGUAGE 'c';
现在定义使用它们的操作符。如前所述,操作符名在所有取两个 int4 操作数的操作符中必须唯一。为了查看下面列出的操作符名是否已被占用,我们可以在 pg_operator 上做一次查询:
/*
* this query uses the regular expression operator (~)
* to find three-character operator names that end in
* the character &
*/
SELECT *
FROM pg_operator
WHERE oprname ~ '^..&$'::text;
以查看你想要的名字是否已被用于你想要的类型。这里的 重要内容是过程(即上面定义的 C 函数)以及限制和连接选择性函数。你应当直接使用下面所用的那些——注意,小于、等于和大于情形有不同的此类函数。必须提供这些函数,否则访问方法在试图使用该操作符时将会崩溃。你应当照抄 restrict 和 join 的名称,但使用你在上一步中定义的过程名。
CREATE OPERATOR = (
leftarg = complex_abs, rightarg = complex_abs,
procedure = complex_abs_eq,
restrict = eqsel, join = eqjoinsel
)
注意,这里定义了对应于小于、小于等于、等于、大于和大于等于的五个操作符。
我们快完成了。最后要做的事情是更新 pg_amop 关系。为此,我们需要下列属性:
表 37.3. pg_amproc 模式
| 属性 | 描述 |
|---|---|
| amopid | pg_am 中 B-tree 实例的 oid (== 403,见上文) |
| amopclaid | complex_abs_ops 的 pg_opclass 实例的 oid (== 你得到的那个替代 17314 的值,见上文) |
| amopopr | 该操作符类的各操作符的 oid (我们马上就会取得) |
| amopselect, amopnpages | 代价函数 |
代价函数供查询优化器用来决定在一次扫描中是否使用某个索引。幸运的是,这些函数已经存在。我们将使用的两个函数是 btreesel(它估计 B-tree 的选择性)和 btreenpage(它估计一次搜索将在树中触及的页面数)。
所以我们需要刚才定义的这些操作符的 oid。我们将查找所有取两个 complex 类型操作数的操作符的名称,并挑出我们的那些:
SELECT o.oid AS opoid, o.oprname
INTO TABLE complex_ops_tmp
FROM pg_operator o, pg_type t
WHERE o.oprleft = t.oid and o.oprright = t.oid
and t.typname = 'complex_abs';
+------+---------+
|oid | oprname |
+------+---------+
|17321 | < |
+------+---------+
|17322 | <= |
+------+---------+
|17323 | = |
+------+---------+
|17324 | >= |
+------+---------+
|17325 | > |
+------+---------+
(同样,你的某些 oid 号几乎肯定会有所不同。)我们感兴趣的操作符是 oid 从 17321 到 17325 的那些。你得到的值很可能不同,你应当用它们替换下文中的值。我们将用一条 select 语句来完成这件事。
现在我们准备好用新操作符类来更新 pg_amop 了。在整个讨论中最重要的一点是:在 pg_amop 中,操作符是按从小等于到大等于排序的。我们添加所需的实例:
INSERT INTO pg_amop (amopid, amopclaid, amopopr, amopstrategy,
amopselect, amopnpages)
SELECT am.oid, opcl.oid, c.opoid, 1,
'btreesel'::regproc, 'btreenpage'::regproc
FROM pg_am am, pg_opclass opcl, complex_abs_ops_tmp c
WHERE amname = 'btree' AND
opcname = 'complex_abs_ops' AND
c.oprname = '<';
然后对其他操作符照此办理,替换上文第三行中的 "1" 和最后一行中的 "<"。注意顺序:"小于"是 1,"小于等于"是 2,"等于"是 3,"大于等于"是 4,"大于"是 5。
下一步是注册此前在讨论 pg_am 时描述过的"支持例程"。该支持例程的 oid 存储在 pg_amproc 类中,以访问方法 oid 和操作符类 oid 为键。 首先,我们需要在 Postgres 中注册该函数(回想一下,我们把实现该例程的 C 代码放在了实现操作符例程的那个文件的底部):
CREATE FUNCTION complex_abs_cmp(complex, complex)
RETURNS int4
AS 'PGROOT/tutorial/obj/complex.so'
LANGUAGE 'c';
SELECT oid, proname FROM pg_proc
WHERE proname = 'complex_abs_cmp';
+------+-----------------+
|oid | proname |
+------+-----------------+
|17328 | complex_abs_cmp |
+------+-----------------+
(同样,你的 oid 号很可能会有所不同,你应当用你看到的值替换下文中的值。) 我们可以像下面这样添加新实例:
INSERT INTO pg_amproc (amid, amopclaid, amproc, amprocnum)
SELECT a.oid, b.oid, c.oid, 1
FROM pg_am a, pg_opclass b, pg_proc c
WHERE a.amname = 'btree' AND
b.opcname = 'complex_abs_ops' AND
c.proname = 'complex_abs_cmp';
现在我们需要添加一个哈希策略,以便该类型能被索引。为此,我们在 pg_am 中使用另一个类型,但复用同样的操作符。
INSERT INTO pg_amop (amopid, amopclaid, amopopr, amopstrategy,
amopselect, amopnpages)
SELECT am.oid, opcl.oid, c.opoid, 1,
'hashsel'::regproc, 'hashnpage'::regproc
FROM pg_am am, pg_opclass opcl, complex_abs_ops_tmp c
WHERE amname = 'hash' AND
opcname = 'complex_abs_ops' AND
c.oprname = '=';
为了在 where 子句中使用这个索引,我们需要像下面这样修改 pg_operator 类。
UPDATE pg_operator
SET oprrest = 'eqsel'::regproc, oprjoin = 'eqjoinsel'
WHERE oprname = '=' AND
oprleft = oprright AND
oprleft = (SELECT oid FROM pg_type WHERE typname = 'complex_abs');
UPDATE pg_operator
SET oprrest = 'neqsel'::regproc, oprjoin = 'neqjoinsel'
WHERE oprname = '' AND
oprleft = oprright AND
oprleft = (SELECT oid FROM pg_type WHERE typname = 'complex_abs');
UPDATE pg_operator
SET oprrest = 'neqsel'::regproc, oprjoin = 'neqjoinsel'
WHERE oprname = '' AND
oprleft = oprright AND
oprleft = (SELECT oid FROM pg_type WHERE typname = 'complex_abs');
UPDATE pg_operator
SET oprrest = 'intltsel'::regproc, oprjoin = 'intltjoinsel'
WHERE oprname = '<' AND
oprleft = oprright AND
oprleft = (SELECT oid FROM pg_type WHERE typname = 'complex_abs');
UPDATE pg_operator
SET oprrest = 'intltsel'::regproc, oprjoin = 'intltjoinsel'
WHERE oprname = '<=' AND
oprleft = oprright AND
oprleft = (SELECT oid FROM pg_type WHERE typname = 'complex_abs');
UPDATE pg_operator
SET oprrest = 'intgtsel'::regproc, oprjoin = 'intgtjoinsel'
WHERE oprname = '>' AND
oprleft = oprright AND
oprleft = (SELECT oid FROM pg_type WHERE typname = 'complex_abs');
UPDATE pg_operator
SET oprrest = 'intgtsel'::regproc, oprjoin = 'intgtjoinsel'
WHERE oprname = '>=' AND
oprleft = oprright AND
oprleft = (SELECT oid FROM pg_type WHERE typname = 'complex_abs');
最后(终于!)我们为该类型注册一段描述。
INSERT INTO pg_description (objoid, description)
SELECT oid, 'Two part G/L account'
FROM pg_type WHERE typname = 'complex_abs';
译文有误、术语不当或页面显示问题,请到译文仓库 pgsty/pgdoc 报告译文问题。 英文原文本身的问题,请在当前版本的对应页面向上游反馈;上游不再修订已结束维护的版本。