R-tree for points

第一个示例涉及在平面上对点(或其他几何形状)进行索引。常规的B树无法用于这种数据类型,因为点没有定义比较运算符。显然,我们可以自己实现这样的运算符,但几何形状需要索引支持完全不同的操作。我将只讨论其中的两种:搜索特定区域内包含的对象和最近邻搜索。

R树在平面上绘制矩形;这些矩形合起来必须覆盖所有被索引的点。一个索引条目存储边界框,谓词可以定义如下:点位于该边界框内。

R树的根节点包含若干个大矩形(这些矩形可能会有重叠)。子节点包含较小的矩形,这些矩形适合其父节点;它们一起覆盖所有底层的点。

叶节点应该包含被索引的点本身,但GiST要求所有条目具有相同的数据类型;因此,叶节点的条目也由矩形表示,这些矩形被简化为点。

为了更好地可视化这种结构,我们来看一个基于机场坐标构建的R树的三个层次。在这个例子中,我已将演示数据库的机场表扩展到五千行。同时我还降低了fillfactor值使树的层次更深。默认值会给我们一个单层树。

1
2
3
4
5
6
demo=# CREATE TABLE airports_big AS select * from airports_data;
SELECT 104
demo=# COPY airports_big FROM '/home/postgres/data/extra_airports.copy';
COPY 5413
demo=# CREATE INDEX airports_gist_idx ON airports_big USING gist(coordinates) WITH (fillfactor=10);
CREATE INDEX

在顶层,所有点都被包含在几个(可能部分重叠的)边界框中。

在下一层,大矩形被分割成较小的矩形。

最后,在树的底层,每个边界框包含的点数量与单个页面所能容纳的数量相同。

该索引使用point_ops操作符类,这是点数据唯一可用的操作符类。

矩形和其他几何形状可以以相同的方式进行索引,但索引存储的不是对象本身,而是其边界框。

Page Layout

可以通过 pageinspect 插件来学习Gist页

和B-Tree索引不同,Gist没有metapage,0号page就是gist的root节点。如果root节点分裂了,老root节点会被move到一个(或多个)单独的页,新root节点取代其位置

root页的内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
demo=# SELECT ctid, keys FROM gist_page_items(get_raw_page('airports_gist_idx', 0), 'airports_gist_idx' );
ctid | keys
------------+--------------------------------------------------------------------------------------------------
(1,65535) | (coordinates)=("(-75.47339630130001,-0.12295600026845932),(-179.876998901,-43.810001373291016)")
(2,65535) | (coordinates)=("(-62.919601,-0.14835),(-74.9615020752,-54.93109893798828)")
(3,65535) | (coordinates)=("(-39.253101348877,-17.652299880981),(-62.1693,-62.1907997131)")
(4,65535) | (coordinates)=("(-14.3937,-0.6994310021400452),(-61.8465003967,-16.706899642899998)")
(5,65535) | (coordinates)=("(22.4692001343,-2.7174999713897705),(10.674076080322266,-34.554901123)")
(6,65535) | (coordinates)=("(63.361,-2.3089799880981445),(22.9993000031,-34.0881601675)")
(7,65535) | (coordinates)=("(177.97799682617188,-31.94029998779297),(115.401596069,-46.8997)")
(8,65535) | (coordinates)=("(-79.3834991455,28.20170021057129),(-177.38099670410156,0.9785190224647522)")
(9,65535) | (coordinates)=("(-51.0722007751,19.96980094909668),(-78.7492,0.0506640002131)")
(10,65535) | (coordinates)=("(128.707992554,-0.06239889934659004),(8.754380226135254,-30.83810043334961)")
(11,65535) | (coordinates)=("(153.56199646,-0.8918330073356628),(130.6199951171875,-31.8885993958)")
(12,65535) | (coordinates)=("(179.951004028,-0.547458),(154.67300415039062,-31.5382995605)")
(13,65535) | (coordinates)=("(-153.7039948,71.285402),(-176.64599609375,51.87799835205078)")
(14,65535) | (coordinates)=("(-128.576009,70.33080291750001),(-152.621994019,53.25429916379999)")
(15,65535) | (coordinates)=("(-104.26300048828125,39.90879822),(-122.8130035,24.072700500499998)")
(16,65535) | (coordinates)=("(-91.149597,37.9275016785),(-103.602996826,25.54949951171875)")
(17,65535) | (coordinates)=("(-68.36340332030001,31.9512996674),(-90.25800323486328,18.25149917602539)")
(18,65535) | (coordinates)=("(-2.269860029220581,31.9475002289),(-67.14849853515625,4.3790202140808105)")
(19,65535) | (coordinates)=("(-115.78199768066,63.20940017700195),(-127.36699676513672,40.1506996155)")
(20,65535) | (coordinates)=("(-96.6707992553711,62.462799072265625),(-114.903999329,37.649899)")
(21,65535) | (coordinates)=("(-83.29579926,37.24570084),(-95.984596252441,32.30059814)")
(22,65535) | (coordinates)=("(-83.3467025756836,48.06570053),(-96.38439941,37.74010086)")
(23,65535) | (coordinates)=("(-78.31999969,47.697400654599996),(-83.072998,32.00999832)")
(24,65535) | (coordinates)=("(-64.67859649658203,47.990799),(-77.98459625,32.36399841308594)")
(25,65535) | (coordinates)=("(-74.5280990600586,79.9946975708),(-126.7979965209961,48.0532989502)")
(26,65535) | (coordinates)=("(-13.746399879455566,82.51779937740001),(-72.2656021118164,32.697899)")
(27,65535) | (coordinates)=("(25.3379993439,39.828335),(-9.35523,0.0226000007242)")
(28,65535) | (coordinates)=("(1.7605600357100002,48.069499969499994),(-8.68138980865,40.471926)")
(29,65535) | (coordinates)=("(1.954759955406189,62.0635986328125),(-9.653610229492188,48.447898864746094)")
(30,65535) | (coordinates)=("(173.82899475097656,31.3253993988),(27.221700668300002,0.042386)")
(31,65535) | (coordinates)=("(7.8790798,61.583599090576),(2.040833,32.38410186767578)")
(32,65535) | (coordinates)=("(58.890499114990234,31.989853),(32.2400016784668,10.350000381469727)")
(33,65535) | (coordinates)=("(89.4672012329,31.909400939941406),(60.3820991516,8.30148983002)")
(34,65535) | (coordinates)=("(111.63999939,31.4281005859375),(90.30120086669922,8.09912014008)")
(35,65535) | (coordinates)=("(169.852005,31.919701),(113.069999695,8.178509712219238)")
(36,65535) | (coordinates)=("(17.439699172973633,52.59120178222656),(6.504444,32.6635017395)")
(37,65535) | (coordinates)=("(31.1583003998,52.527000427246094),(17.828399658203125,32.096801757799994)")
(38,65535) | (coordinates)=("(30.60810089111328,57.78390121459961),(6.57944011688,53.0475006104)")
(39,65535) | (coordinates)=("(31.044900894165,78.246101379395),(6.0741000175476,58.0994987487793)")
(40,65535) | (coordinates)=("(42.4826011658,61.88520050048828),(31.9197998046875,32.01139831542969)")
(41,65535) | (coordinates)=("(75.634444,63.566898345947266),(43.025978088378906,32.056098938)")
(42,65535) | (coordinates)=("(98.3414,73.51780700683594),(32.75080108642578,32.1)")
(43,65535) | (coordinates)=("(122.853996,71.97810363769531),(100.0989990234375,32.1506)")
(44,65535) | (coordinates)=("(177.74099731445312,71.697700500488),(123.48300170898438,32.482498)")
(44 rows)

To extract more detailed information, you can use the gevel extension,which is not included into the standard PostgreSQL distribution.

Operator Class

This query retrieves the list of support functions used by the point_ops operator class in a GiST (Generalized Search Tree) index within a PostgreSQL database

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
demo=# SELECT amprocnum, amproc::regproc
FROM pg_am am
JOIN pg_opclass opc ON opcmethod = am.oid
JOIN pg_amproc amop ON amprocfamily = opcfamily
WHERE amname = 'gist'
AND opcname = 'point_ops'
ORDER BY amprocnum;
amprocnum | amproc
-----------+------------------------
1 | gist_point_consistent
2 | gist_box_union
3 | gist_point_compress
5 | gist_box_penalty
6 | gist_box_picksplit
7 | gist_box_same
8 | gist_point_distance
9 | gist_point_fetch
11 | gist_point_sortsupport
(9 rows)

必需的函数:

1 consistency function used to traverse the tree during search(检查查询条件是否与索引条目一致)

2 union function that merges rectangles计算边界框的并集)

5 penalty function used to choose the subtree to descend to when inserting an entry (计算插入新条目的代价)

6 picksplit function that distributes entries between new pages after a page split(决定节点分裂方式)

7 same function that checks two keys for equality(比较索引条目是否相同)

point_ops 支持的操作符如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
demo=# SELECT amopopr::regoperator, amopstrategy AS  st, oprcode::regproc,
left(obj_description(opr.oid, 'pg_operator'),19) description
FROM pg_am am
JOIN pg_opclass opc ON opcmethod = am.oid
JOIN pg_amop amop ON amopfamily = opcfamily
JOIN pg_operator opr ON opr.oid = amopopr
WHERE amname = 'gist'
AND opcname = 'point_ops'
ORDER BY amopstrategy;
amopopr | st | oprcode | description
-------------------+----+---------------------+---------------------
<<(point,point) | 1 | point_left | is left of
>>(point,point) | 5 | point_right | is right of
~=(point,point) | 6 | point_eq | same as
<<|(point,point) | 10 | point_below | is below
|>>(point,point) | 11 | point_above | is above
<->(point,point) | 15 | point_distance | distance between
<@(point,box) | 28 | on_pb | point inside box
<^(point,point) | 29 | point_below | deprecated, use <<|
>^(point,point) | 30 | point_above | deprecated, use |>>
<@(point,polygon) | 48 | pt_contained_poly | is contained by
<@(point,circle) | 68 | pt_contained_circle | is contained by
(11 rows)

操作符的名字通常并不能准确反映其语义,因此这个查询还会显示底层函数的名称和它们的描述。无论具体形式如何,这些操作符都处理几何对象之间的相对位置关系(如在左侧、右侧、上方、下方、包含、被包含)以及它们之间的距离。
与 B-tree 相比,GiST 提供了更多的策略(strategy)。一些策略号在多种类型的索引中是通用的,而另一些则是通过公式计算出来的(例如,策略号 28、48 和 68 实际上代表相同的策略:对矩形、多边形和圆形来说都是“被包含”)。此外,GiST 还支持一些已经过时的操作符名称(例如 <<| 和 |>>)。
一个操作符类(operator class)可能只实现了部分可用的策略。举个例子:点类型的操作符类不支持“包含”这个策略,但这个策略在那些具有可度量面积的几何体操作符类(如 box_ops、poly_ops 和 circle_ops)中是可用的。

Search for Contained Elements

一个典型的可以通过索引加速的查询是返回指定区域内的所有点。例如,我们来查找距离莫斯科中心一度以内的所有机场:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
demo=# SELECT airport_code, airport_name->>'en'
FROM airports_big
WHERE coordinates <@ '<(37.622513,55.753220),1.0>'::circle;
airport_code | ?column?
--------------+------------------------------------
SVO | Sheremetyevo International Airport
VKO | Vnukovo International Airport
DME | Domodedovo International Airport
BKA | Bykovo Airport
ZIA | Zhukovsky International Airport
CKL | Chkalovskiy Air Base
OSF | Ostafyevo International Airport
(7 rows)

demo=# EXPLAIN (costs off) SELECT airport_code
FROM airports_big
WHERE coordinates <@ '<(37.622513,55.753220),1.0>'::circle;
QUERY PLAN
-------------------------------------------------------------------------
Bitmap Heap Scan on airports_big
Recheck Cond: (coordinates <@ '<(37.622513,55.75322),1>'::circle)
-> Bitmap Index Scan on airports_gist_idx
Index Cond: (coordinates <@ '<(37.622513,55.75322),1>'::circle)
(4 rows)

我们可以通过下图所示的一个简单示例来更仔细地查看这个操作符

rtree1

如果以这种方式选择边界框,索引结构将如下所示:

rtree2

包含操作符 <@ 用于判断某个点是否位于指定矩形内部。对于该操作符,其一致性函数(consistency function)会在索引条目的矩形与指定矩形有任何重合点时返回“是”。这意味着,对于叶子节点中的索引条目(它们通常是退化为点的矩形),该函数实际上是在判断这个点是否被指定的矩形所包含。

例如,假设我们要查找位于矩形 (1,2)–(4,7) 内部的点,该矩形在下图中以阴影表示:

rtree3

搜索从根节点开始。目标矩形与索引项 (0,0)–(3,4) 有重叠,但与 (5,3)–(9,9) 没有重叠,这意味着我们不需要进入第二棵子树。
在下一层中,目标矩形与 (0,3)–(3,4) 有重叠,并且与 (0,0)–(3,2) 有接触(边界相交),所以我们需要检查这两个子树。
一旦到达叶子节点,我们只需遍历它们包含的所有点,并返回那些满足一致性函数的点

rtree4

B-tree 的搜索总是只选择一个子节点进行查找。而 GiST 的搜索可能需要扫描多个子树,特别是当它们的边界框(bounding boxes)发生重叠时。

大多数索引支持的操作符(例如上一个例子中的 = 或 <@)通常被称为搜索操作符,因为它们在查询中定义了搜索条件。这类操作符是谓词,即它们返回一个逻辑值(真或假)。

但还有一类是排序操作符,它们返回的是参数之间的距离。这类操作符通常用于 ORDER BY 子句中,并且一般由具有 Distance Orderable 属性的索引所支持。该属性允许你快速找到指定数量的最近邻。这种类型的搜索被称为 k-NN(k 最近邻搜索)。

例如,我们可以查找最接近 Kostroma 的 10 个机场:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
demo=# SELECT airport_code, airport_name->>'en'
FROM airports_big
ORDER BY coordinates <-> '(40.926780,57.767943)'::point LIMIT 10;
airport_code | ?column?
--------------+------------------------------------------------
KMW | Kostroma Sokerkino Airport
IAR | Tunoshna Airport
IWA | Ivanovo South Airport
VGD | Vologda Airport
RYB | Staroselye Airport
GOJ | Nizhny Novgorod Strigino International Airport
CEE | Cherepovets Airport
CKL | Chkalovskiy Air Base
ZIA | Zhukovsky International Airport
BKA | Bykovo Airport
(10 rows)

demo=# EXPLAIN (costs off) SELECT airport_code
FROM airports_big
ORDER BY coordinates <-> '(40.926780,57.767943)'::point LIMIT 5;
QUERY PLAN
-------------------------------------------------------------------
Limit
-> Index Scan using airports_gist_idx on airports_big
Order By: (coordinates <-> '(40.92678,57.767943)'::point)
(3 rows)

由于索引扫描是逐个返回结果,并且可以在任何时刻停止,因此前几个值可以非常快速地获取到。

如果没有索引支持,要实现高效的搜索将非常困难。我们将不得不先查找某个特定区域内的所有点,然后逐步扩大该区域,直到返回所需数量的结果。
这将需要多次索引扫描,更不用说还存在一个难题:如何选择初始区域的大小以及每次扩展的增量。

你可以在系统目录中查看操作符的类型(其中 “s” 表示搜索操作符,”o” 表示排序操作符)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
demo=# SELECT amopopr::regoperator, amoppurpose, amopstrategy FROM pg_am am
JOIN pg_opclass opc ON opcmethod = am.oid
JOIN pg_amop amop ON amopfamily = opcfamily WHERE amname = 'gist'
AND opcname = 'point_ops'
ORDER BY amopstrategy;
amopopr | amoppurpose | amopstrategy
-------------------+-------------+--------------
<<(point,point) | s | 1
>>(point,point) | s | 5
~=(point,point) | s | 6
<<|(point,point) | s | 10
|>>(point,point) | s | 11
<->(point,point) | o | 15
<@(point,box) | s | 28
<^(point,point) | s | 29
>^(point,point) | s | 30
<@(point,polygon) | s | 48
<@(point,circle) | s | 68
(11 rows)

为了支持这类查询,操作符类必须定义一个额外的支持函数:也就是距离函数(distance function),它会在索引项上被调用,用于计算该索引项中存储的值与另一个值之间的距离。

对于表示索引值的叶子节点元素,该函数必须返回与该值之间的距离。
如果是点(point)类型,这个距离就是常规的欧几里得距离,计算公式为:

1
d = sqrt((x₂-x₁)²+(y₂-y₁)²)

对于一个内部节点,其距离函数必须返回其所有子叶节点中可能距离的最小值。
由于扫描所有子节点的条目代价较高,该函数可以乐观地低估这个距离(以牺牲效率为代价),但绝不能返回一个较大的值——否则将会破坏搜索的正确性

因此,对于一个由边界框(bounding box)表示的内部节点,其与某个点之间的距离可以按照常规数学意义来理解:
要么是该点到矩形边界的最小距离;要么是 0(如果该点在矩形内部)。
这个值可以在无需遍历矩形中所有子点的情况下轻松计算出来,
并且它保证不会大于矩形中任意一个子点与该查询点之间的实际距离

我们考虑一下寻离点(6,8)最近的3个值

rtree5

搜索从根节点开始,根节点包含两个边界框(bounding box)。指定查询点到矩形 (0,0)–(3,4) 的距离被计算为到该矩形的角点 (3,4) 的距离,即 5.0。到另一个矩形 (5,3)–(9,9) 的距离为 0.0。(这里我会将所有的距离值四舍五入保留到小数点后一位,这种精度对于这个示例来说已经足够。)

子节点会按照距离增大的顺序被遍历。因此,我们首先进入右子节点,该节点包含两个矩形:(5,3)–(8,5) 和 (6,6)–(9,9)。到第一个矩形的距离是 3.0,到第二个矩形的距离是 0.0。
再次地,我们选择右子树,并进入包含三个点的叶子节点:点 (6,6) 的距离为 2.0,点 (8,9) 的距离为 2.2,点 (9,7) 的距离为 3.2。

rtree6

因此,我们已经找到了前两个点:(6,6) 和 (8,9)。但该节点中的第三个点距离(查询点)要大于矩形 (5,3)–(8,5) 的距离。

所以我们需要进入左子节点,该节点包含两个点。到点 (8,5) 的距离是 3.6, 到点 (5,3) 的距离是 5.1。结果发现,之前那个子节点中的点 (9,7) 比左子树中的任何点都更接近查询点 (6,8),因此我们可以将它作为第三个返回结果。

rtree7

这个例子说明了内部条目的距离函数必须满足的要求。由于到矩形 (5,3)–(8,5) 的距离减小(3.0 而不是 3.6),导致需要额外扫描一个节点,因此搜索效率下降;不过,算法本身仍然是正确的。

Insertion

当向 R 树中插入一个新键时,用于存放该键的节点是由penalty函数决定的:该节点的边界框(bounding box)大小应尽可能少地增加。

rtree8

例如,点 (4,7) 将被插入到矩形 (5,3)–(9,9) 中,因为该矩形的面积只需增加 6 个单位;而如果插入到矩形 (0,0)–(3,4),则其面积需要增加 12 个单位。在下一层(叶子层),该点也会按照相同的逻辑被加入到矩形 (6,6)–(9,9) 中。

假设一个页面最多可以容纳三个元素,那么当超出这个限制时,它必须被拆分成两个页面,
并且这些元素需要在新的页面之间重新分配。在这个示例中,分配结果看起来很明显,但在一般情况下,数据的分布并不那么简单。最重要的是,picksplit 函数会尽量减少边界框(bounding box)之间的重叠,目标是获得更小的矩形以及在页面之间均匀分布点。

rtree9

Exclusion Constraints

GiST 索引也可以用于排除约束(exclusion constraints)。排除约束确保:对任意两条堆表元组来说,它们在某些操作符意义下的指定字段不能彼此匹配。要实现这一点,必须满足以下条件:

  • 排除约束(exclusion constraint)必须由索引方法本身支持(即具备 Can Exclude 属性)。
  • 所使用的操作符必须属于该索引方法的操作符类(operator class);
  • 操作符必须是可交换的:即满足 “a operator b = b operator a” 这个条件。

对于上面提到的 hash 和 btree 访问方法来说,唯一合适的操作符是等于(=)。这实际上使排除约束退化成了唯一约束,因而没有太大实际用途

GiST 方法还支持另外两种适用的策略:

  • 重叠(overlapping):由 && 操作符表示
  • 相邻(adjacency):由 -|- 操作符表示(该操作符主要用于区间)

我们来试试这个功能:创建一个约束,用于禁止机场之间距离太近。
这个条件可以这样表达:以机场坐标为圆心、指定半径的圆形不得彼此重叠。

1
2
3
4
5
6
7
8
demo=# ALTER TABLE airports_data ADD EXCLUDE USING gist (circle(coordinates,0.2) WITH &&);
ALTER TABLE
demo=# INSERT INTO airports_data(
airport_code, airport_name, city, coordinates, timezone
) VALUES (
'ZIA', '{}', '{"en": "Moscow"}', point(38.1517, 55.5533), 'Europe/Moscow');
ERROR: conflicting key value violates exclusion constraint "airports_data_circle_excl"
DETAIL: Key (circle(coordinates, 0.2::double precision))=(<(38.1517,55.5533),0.2>) conflicts with existing key (circle(coordinates, 0.2::double precision))=(<(37.90629959106445,55.40879821777344),0.2>).

当定义一个排除约束(exclusion constraint)时,系统会自动创建一个索引来强制执行该约束。在本例中,这个索引是一个基于表达式的 GiST 索引。

我们来看一个更复杂的例子。假设我们允许机场之间距离很近,但前提是这些机场属于同一个城市。一种可行的解决方案是定义一个新的完整性约束,表达如下:如果两个圆(以机场坐标为圆心)发生重叠(&&),且它们对应的城市名称不同(!=),则这种情况是不允许的。

尝试创建这样的约束会导致一个错误,因为对于 text 数据类型并没有可用的操作符类(operator class)。

1
2
3
4
5
6
7
8
9
demo=# ALTER TABLE airports_data
DROP CONSTRAINT airports_data_circle_excl;
ALTER TABLE
demo=# ALTER TABLE airports_data ADD EXCLUDE USING gist ( circle(coordinates,0.2) WITH &&,
(city->>'en') WITH !=
);
ERROR: data type text has no default operator class for access method "gist"
HINT: You must specify an operator class for the index or define a default operator class for the data type.
demo=#

虽然 GiST 原生不支持 text 或 int 等有序类型,但借助 btree_gist 扩展,可以让这些类型也具备使用 GiST 进行等值/比较操作的能力,从而用于复杂约束(如排除约束)或混合类型索引。

1
2
3
4
5
6
7
demo=# CREATE EXTENSION btree_gist;
CREATE EXTENSION
demo=# ALTER TABLE airports_data ADD EXCLUDE USING gist ( circle(coordinates,0.2) WITH &&,
(city->>'en') WITH !=
);
ALTER TABLE
demo=#

该约束已成功创建。现在我们无法添加名为 Zhukovsky 的机场(即使它属于同名城市),
因为它与莫斯科的几个机场之间的距离太近,违反了约束条件。

1
2
3
4
5
6
7
8
demo=# INSERT INTO airports_data(
airport_code, airport_name, city, coordinates, timezone
) VALUES (
'ZIA', '{}', '{"en": "Zhukovsky"}', point(38.1517, 55.5533), 'Europe/Moscow'
);
ERROR: conflicting key value violates exclusion constraint "airports_data_circle_expr_excl"
DETAIL: Key (circle(coordinates, 0.2::double precision), (city ->> 'en'::text))=(<(38.1517,55.5533),0.2>, Zhukovsky) conflicts with existing key (circle(coordinates, 0.2::double precision), (city ->> 'en'::text))=(<(37.90629959106445,55.40879821777344),0.2>, Moscow).
demo=#

但是我们可以在莫斯科创建机场

1
2
3
4
5
6
7
demo=# INSERT INTO airports_data(
airport_code, airport_name, city, coordinates, timezone
) VALUES (
'ZIA', '{}', '{"en": "Moscow"}', point(38.1517, 55.5533), 'Europe/Moscow'
);
INSERT 0 1
demo=#

需要注意的是,尽管 GiST 支持大于、小于和等于等操作符,但 B-tree 在这方面效率要高得多,尤其是在访问一段范围值时。因此,只有当确实有其他合理原因需要使用 GiST 索引时,
才有意义使用上面提到的 btree_gist 扩展技巧。

Properties

访问方法属性(Access Method Properties)以下是 GiST 访问方法的属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
demo=# SELECT a.amname, p.name, pg_indexam_has_property(a.oid, p.name) 
FROM pg_am a, unnest(array[
'can_order', 'can_unique', 'can_multi_col',
'can_exclude', 'can_include'
]) p(name)
WHERE a.amname = 'gist';
amname | name | pg_indexam_has_property
--------+---------------+-------------------------
gist | can_order | f
gist | can_unique | f
gist | can_multi_col | t
gist | can_exclude | t
gist | can_include | t
(5 rows)

GiST 索引不支持唯一约束(Unique constraints)和排序(sorting)。GiST 索引可以通过额外的 INCLUDE 列来创建。正如我们所知,我们可以在多个列上构建索引,也可以将其用于完整性约束(integrity constraints)。

索引级别属性(Index-level properties)。这些属性是在索引层面定义的。

1
2
3
4
5
6
7
8
9
10
11
demo=# SELECT p.name, pg_index_has_property('airports_gist_idx', p.name) 
FROM unnest(array[
'clusterable', 'index_scan', 'bitmap_scan', 'backward_scan'
]) p(name);
name | pg_index_has_property
---------------+-----------------------
clusterable | t
index_scan | t
bitmap_scan | t
backward_scan | f
(4 rows)

GiST 索引可以用于聚簇(clusterization)操作。在数据检索方式方面,GiST 支持常规(逐行)索引扫描和位图扫描(bitmap scan)。但 GiST 不支持反向扫描(backward scanning)。

列级别属性(Column-level properties):大多数列属性是在访问方法(access method)级别定义的,并且保持不变。

1
2
3
4
5
6
7
8
9
10
11
demo=# SELECT p.name, pg_index_column_has_property('airports_gist_idx', 1, p.name)
FROM unnest(array[
'orderable', 'search_array', 'search_nulls'
]) p(name);

name | pg_index_column_has_property
--------------+------------------------------
orderable | f
search_array | f
search_nulls | t
(3 rows)

所有与排序相关的属性都是禁用的。

GiST 索引允许 NULL 值存在,但处理效率并不高。一般认为,NULL 值不会扩展边界框(bounding box),所以这些值会被随机插入某个子树中。因此,在查询时需要遍历整棵 GiST 树来查找这些值。

不过,有少数列级属性是依赖于具体操作符类(operator class)的。

1
2
3
4
5
6
7
8
9
demo=# SELECT p.name, pg_index_column_has_property('airports_gist_idx', 1, p.name)
FROM unnest(array[
'returnable', 'distance_orderable'
]) p(name);
name | pg_index_column_has_property
--------------------+------------------------------
returnable | t
distance_orderable | t
(2 rows)

GiST 索引允许执行索引仅扫描(Index-only scan),因为叶子节点中保留了完整的索引键值。

正如前文所述,某些操作符类(operator class)提供了用于最近邻搜索的距离操作符。
对于 NULL 值,距离计算结果为 NULL,这种情况下这些值会排在最后返回(类似 B-tree 中的 NULLS LAST 语法)。

然而,对于范围类型(range types)而言,并不存在“距离操作符”(因为它们表示的是线段,也就是线性几何体,而不是面状几何体)。所以,当索引建立在这些类型上时,上述性质会有所不同。

1
2
3
4
5
6
7
8
9
10
11
12
13
demo=# CREATE TABLE reservations(during tsrange);
CREATE TABLE
demo=# CREATE INDEX ON reservations USING gist(during);
CREATE INDEX
demo=# SELECT p.name, pg_index_column_has_property('reservations_during_idx', 1, p.name)
FROM unnest(array[
'returnable', 'distance_orderable'
]) p(name);
name | pg_index_column_has_property
--------------------+------------------------------
returnable | t
distance_orderable | f
(2 rows)

参考书目

  1. Egor Rogov, PostgreSQL 14 Internals, https://postgrespro.com/community/books/internals