PostgreSQL中的索引—6(SP-GiST)

目录

SP-GiST

架构

示例:四叉树

内部构件

示例:k维树

内部构件

示例:基数树

内部构件

属性

NULL

其他数据类型


我们已经介绍了Postgresql索引引擎访问方法接口和三种方法:哈希索引B树索引GiST。在这边文章中,我们将描述SP-GiST。

SP-GiST

(通常用于多维空间)首先,对这个名字说几句话。“GiST”部分暗示了与同名访问方法的一些相似之处。这种相似性确实存在:两者都是通用的搜索树,为构建各种访问方法提供了框架。

“SP”代表空间分区。这里的空间通常就是我们所说的空间,比如二维平面。但我们将看到,任何搜索空间都是有意义的,也就是说,实际上是任何值域。

SP-GiST适用于空间可以递归分割为非相交区域的结构。该类包括四叉树、k维树(k-D树)和基数树。

架构

(非平衡树、少节点多深度)因此,SP-GiST访问方法的思想是将值域拆分为不重叠的子域(GiST可能是部分重叠的),每个子域也可以拆分。这样的划分会导致非平衡树(与B-树和常规GiST不同)。

不相交的特性简化了插入和搜索过程中的决策。另一方面,通常情况下,诱导的树木分枝较少。例如,四叉树的一个节点通常有四个子节点(不同于B-树,B-树中的节点多达数百个)和更大的深度。这样的树很适合在RAM中工作,但索引存储在磁盘上,因此,为了减少I/O操作的数量,必须将节点打包到页面中,而要有效地做到这一点并不容易。此外,在索引中找到不同值所需的时间可能会因分支深度的不同而有所不同。

这种访问方法与GiST的方式相同,也考虑了底层任务(同时访问和锁定、日志记录和纯搜索算法),并提供了一个专门的简化接口,以支持添加对新数据类型和新分区算法的支持。

(节点结构与GiST基本相同)SP-GiST树的内部节点存储对子节点的引用;可以为每个引用定义一个标签。此外,内部节点可以存储一个名为前缀的值。实际上,这个值不是前缀的强制值;它可以被视为所有子节点都满足的任意谓词。

SP-GiST的叶节点包含索引类型的值和对表行(TID)的引用。索引数据本身(搜索键)可以用作值,但不是必须的:可以存储缩短的值。

此外,叶节点可以分组到列表中。因此,内部节点不仅可以引用一个值,还可以引用整个列表。

请注意,叶节点中的前缀、标签和值都有各自的数据类型,彼此独立。

和GiST一样,定义搜索的主要功能是一致性函数。该函数针对树节点调用,并返回一组值与搜索谓词“一致”的子节点(通常以"indexed-field operator expression"的形式)。对于叶节点,一致性函数确定该节点中的索引值是否满足搜索谓词。

搜索从根节点开始。一致性函数允许找出访问哪些子节点是有意义的。对于找到的每个节点,该算法都会重复。搜索是深度优先

在物理层,索引节点被打包到页面中,以便从I/O操作的角度有效地使用节点。请注意,一个页面可以包含内部节点或叶节点,但不能同时包含两者。

示例:四叉树

四叉树用于索引平面中的点。一个想法是,相对于中心点,递归地将区域分割成四个部分(象限)。这种树的分支深度可能会有所不同,并且取决于适当象限中的点密度。

这是它在图中的样子,以openflights网站上机场增强的演示数据库为例。顺便说一句,最近我们发布了一个新版本的数据库,其中,我们用一个“点”类型的字段替换了经度和纬度。

首先,我们把飞机分成四个象限。。。

然后我们把每个象限分开…… 

 依此类推,直到我们得到最终的分区。

让我们提供一个简单例子的更多细节,我们已经在GiST相关文章中考虑过了。看看在这种情况下分区会是什么样子:

象限的编号如第一幅图所示。为了明确起见,让我们将子节点从左到右完全按照相同的顺序放置。下图显示了这种情况下可能的索引结构。每个内部节点最多引用四个子节点。每个引用都可以用象限号标记,如图所示。但是在实现中没有标签,因为存储四个引用的固定数组更方便,其中一些引用可以是空的。

位于边界上的点与数字较小的象限相关。(边界上的点属于较小的象限)

postgres=# create table points(p point);

postgres=# insert into points(p) values
  (point '(1,1)'), (point '(3,2)'), (point '(6,3)'),
  (point '(5,5)'), (point '(7,8)'), (point '(8,6)');

postgres=# create index points_quad_idx on points using spgist(p);

在这种情况下,默认情况下使用“quad_point_ops”操作符类,其中包含以下操作符:

postgres=# select amop.amopopr::regoperator, amop.amopstrategy
from pg_opclass opc, pg_opfamily opf, pg_am am, pg_amop amop
where opc.opcname = 'quad_point_ops'
and opf.oid = opc.opcfamily
and am.oid = opf.opfmethod
and amop.amopfamily = opc.opcfamily
and am.amname = 'spgist'
and amop.amoplefttype = opc.opcintype;

     amopopr     | amopstrategy
-----------------+--------------
 <<(point,point) |            1  strictly left
 >>(point,point) |            5  strictly right
 ~=(point,point) |            6  coincides
 <^(point,point) |           10  strictly below
 >^(point,point) |           11  strictly above
 <@(point,box)   |            8  contained in rectangle
(6 rows)

例如,让我们看看查询 select * from points where p >^ point '(2,7)' 将如何执行(查找位于给定点之上的所有点)。

我们从根节点开始,使用一致性函数选择要下降到哪个子节点。对于运算符>^,此函数将点(2,7)与节点(4,4)的中心点进行比较,并选择可能包含所需点的象限,在本例中为第一和第四象限。

在对应于第一象限的节点中,我们再次使用一致性函数确定子节点。中心点是(6,6),我们再次需要查看第一象限和第四象限。

叶节点列表(8,6)和(7,8)对应于第一个象限,其中只有点(7,8)满足查询条件。对第四象限的引用为空。

在内部节点(4,4)中,对第四象限的引用也是空的,这就完成了搜索。(所以四叉树的优势是不需要扫描所有象限的点)

postgres=# set enable_seqscan = off;

postgres=# explain (costs off) select * from points where p >^ point '(2,7)';

                   QUERY PLAN                  
------------------------------------------------
 Index Only Scan using points_quad_idx on points
   Index Cond: (p >^ '(2,7)'::point)
(2 rows)

内部构件

我们可以使用前面提到的“gevel”扩展来探索SP-GiST索引的内部结构。坏消息是,由于一个bug,这个扩展在现代版本的PostgreSQL中无法正常工作。好消息是,我们计划用“gevel”(讨论)的功能来增强“pageinspect”。该漏洞已在“pageinspect”中修复。

【同样,坏消息是,补丁一直没有进展。】

例如,让我们以扩展的演示数据库为例,它用于使用世界地图绘制图片。

demo=# create index airports_coordinates_quad_idx on airports_ml using spgist(coordinates);

首先,我们可以得到该索引的一些统计数据:

demo=# select * from spgist_stats('airports_coordinates_quad_idx');

           spgist_stats           
----------------------------------
 totalPages:        33           +
 deletedPages:      0            +
 innerPages:        3            +
 leafPages:         30           +
 emptyPages:        2            +
 usedSpace:         201.53 kbytes+
 usedInnerSpace:    2.17 kbytes  +
 usedLeafSpace:     199.36 kbytes+
 freeSpace:         61.44 kbytes +
 fillRatio:         76.64%       +
 leafTuples:        5993         +
 innerTuples:       37           +
 innerAllTheSame:   0            +
 leafPlaceholders:  725          +
 innerPlaceholders: 0            +
 leafRedirects:     0            +
 innerRedirects:    0
(1 row)

第二,我们可以输出索引树本身:

demo=# select tid, n, level, tid_ptr, prefix, leaf_value
from spgist_print('airports_coordinates_quad_idx') as t(
  tid tid,
  allthesame bool,
  n int,
  level int,
  tid_ptr tid,
  prefix point,    -- prefix type
  node_label int,  -- label type (unused here)
  leaf_value point -- list value type
)
order by tid, n;

   tid   | n | level | tid_ptr |      prefix      |    leaf_value
---------+---+-------+---------+------------------+------------------
 (1,1)   | 0 |     1 | (5,3)   | (-10.220,53.588) |
 (1,1)   | 1 |     1 | (5,2)   | (-10.220,53.588) |
 (1,1)   | 2 |     1 | (5,1)   | (-10.220,53.588) |
 (1,1)   | 3 |     1 | (5,14)  | (-10.220,53.588) |
 (3,68)  |   |     3 |         |                  | (86.107,55.270)
 (3,70)  |   |     3 |         |                  | (129.771,62.093)
 (3,85)  |   |     4 |         |                  | (57.684,-20.430)
 (3,122) |   |     4 |         |                  | (107.438,51.808)
 (3,154) |   |     3 |         |                  | (-51.678,64.191)
 (5,1)   | 0 |     2 | (24,27) | (-88.680,48.638) |
 (5,1)   | 1 |     2 | (5,7)   | (-88.680,48.638) |
 ...

但请记住,“spgist_print”并不是输出所有叶值,而是只输出列表中的第一个叶值,因此显示的是索引的结构,而不是其全部内容。

示例:k维树

(可以执行空间中的二分法)对于平面上的相同点,我们还可以建议另一种划分空间的方法。

让我们画一条水平线穿过第一个被索引的点。它将平面分成两部分:上部和下部。第二个要索引的点属于这些部分之一,通过这一点,我们画一条垂直线,将这部分分成两部分:右侧和左侧。我们再次画一条穿过下一点的水平线和一条穿过下一点的垂直线,以此类推。

以这种方式构建的树的所有内部节点将只有两个子节点。这两个引用中的每一个都可以指向层次结构中下一个内部节点或者叶节点列表。

这种方法可以很容易地推广到k维空间(上面的描述与下面的示例都是二维的),因此,在文献中,这些树也被称为k维树(k-D树)。

以机场为例说明该方法:

首先我们把飞机分成上下两部分……

然后我们把每个部分分成左右两部分……

依此类推,直到我们得到最终的分区。(所以这个点的选择是完全随机的还是按照数据在表中的排序,应该是随机选的点吧?)

要像这样使用分区,我们需要在创建索引时显式指定操作符类“kd_point_ops”。

postgres=# create index points_kd_idx on points using spgist(p kd_point_ops);

该(操作符)类包含与“默认”类“四点操作”完全相同的运算符。

内部构件

在查看树结构时,我们需要考虑到,在这种情况下,前缀只是一个坐标,而不是一个点(坐标不就是点吗?)

demo=# select tid, n, level, tid_ptr, prefix, leaf_value
from spgist_print('airports_coordinates_kd_idx') as t(
  tid tid,
  allthesame bool,
  n int,
  level int,
  tid_ptr tid,
  prefix float,    -- prefix type
  node_label int,  -- label type (unused here)
  leaf_value point -- list node type
)
order by tid, n;

   tid   | n | level | tid_ptr |   prefix   |    leaf_value
---------+---+-------+---------+------------+------------------
 (1,1)   | 0 |     1 | (5,1)   |     53.740 |
 (1,1)   | 1 |     1 | (5,4)   |     53.740 |
 (3,113) |   |     6 |         |            | (-7.277,62.064)
 (3,114) |   |     6 |         |            | (-85.033,73.006)
 (5,1)   | 0 |     2 | (5,12)  |    -65.449 |
 (5,1)   | 1 |     2 | (5,2)   |    -65.449 |
 (5,2)   | 0 |     3 | (5,6)   |     35.624 |
 (5,2)   | 1 |     3 | (5,3)   |     35.624 |
 ...

示例:基数树

我们还可以使用SP GiST实现字符串的基数树。基数树的概念是,要索引的字符串不是完全存储在叶节点中,而是通过将存储在该节点上方的节点中的值连接到根来获得

假设,我们需要索引站点URL:“postgrespro.ru”、“postgrespro.com”、“postgresql.org”和“planet.postgresql.org”。

postgres=# create table sites(url text);

postgres=# insert into sites values ('postgrespro.ru'),('postgrespro.com'),('postgresql.org'),('planet.postgresql.org');

postgres=# create index on sites using spgist(url);

(这个树的数据查询像那个什么编码)这棵树将如下所示:

树的内部节点存储所有子节点共用的前缀。例如,在“stgres”的子节点中,值以“p”+“o”+“stgres”开头。

与四叉树不同,指向子节点的每个指针都附加了一个字符(更准确地说,是两个字节,但这并不重要)。

“text_ops”操作符类支持B树式操作符:“等于”、“大于”和“小于”:

postgres=# select amop.amopopr::regoperator, amop.amopstrategy
from pg_opclass opc, pg_opfamily opf, pg_am am, pg_amop amop
where opc.opcname = 'text_ops'
and opf.oid = opc.opcfamily
and am.oid = opf.opfmethod
and amop.amopfamily = opc.opcfamily
and am.amname = 'spgist'
and amop.amoplefttype = opc.opcintype;

     amopopr     | amopstrategy
-----------------+--------------
 ~<~(text,text)  |            1
 ~<=~(text,text) |            2
 =(text,text)    |            3
 ~>=~(text,text) |            4
 ~>~(text,text)  |            5
 <(text,text)    |           11
 <=(text,text)   |           12
 >=(text,text)   |           14
 >(text,text)    |           15
(9 rows)

带波浪号的运算符的区别在于,它们操作字节而不是字符。

有时,基树形式的表示可能比B树更紧凑,因为值没有完全存储,而是在需要时在树中下降重建。 考虑一个查询:select * from sites where url like 'postgresp%ru'。可以使用以下索引执行:

postgres=# explain (costs off) select * from sites where url like 'postgresp%ru';

                                  QUERY PLAN                                  
------------------------------------------------------------------------------
 Index Only Scan using sites_url_idx on sites
   Index Cond: ((url ~>=~ 'postgresp'::text) AND (url ~<~ 'postgresq'::text))
   Filter: (url ~~ 'postgresp%ru'::text)
(3 rows)

实际上,索引用于查找大于或等于“postgresp”,但小于“postgresq”(索引Cond)的值,然后从结果中选择匹配值(过滤器)。

首先,一致性函数必须决定我们需要下降到“p”根的哪个子节点。有两个选项可用:“p”+“l”(无需下降,这点即使不深入下降也很清楚)和“p”+“o”+“stgres”(继续下降)。

对于“stgres”节点,需要再次调用一致性函数来检查“postgres”+“p”+“ro”(继续下降)和“postgres”+“q”(无需下降)。

对于“ro”节点及其所有子叶节点,一致性函数将响应“yes”,因此index方法将返回两个值:“postgrespro.com”和“postgrespro.ru”。在筛选阶段,将从中选择一个匹配值。 

内部构件

在查看树结构时,我们需要考虑数据类型:

postgres=# select * from spgist_print('sites_url_idx') as t(
  tid tid,
  allthesame bool,
  n int,
  level int,
  tid_ptr tid,
  prefix text,         -- prefix type
  node_label smallint, -- label type
  leaf_value text      -- leaf node type
)
order by tid, n;

属性

让我们看看SP-GiST访问方法的属性(之前提供了查询方法):

 amname |     name      | pg_indexam_has_property
--------+---------------+-------------------------
 spgist | can_order     | f
 spgist | can_unique    | f
 spgist | can_multi_col | f
 spgist | can_exclude   | t

SP-GiST索引不能用于排序和支持唯一约束。此外,这样的索引不能在多个列上创建(与GiST不同)(为什么)。但允许使用此类索引来支持排除约束(因为各个节点没有重叠,但不重叠难道不就是支持唯一约束吗?)

以下索引层特性可用:

        name        | pg_index_column_has_property 
--------------------+------------------------------
 asc                | f
 desc               | f
 nulls_first        | f
 nulls_last         | f
 orderable          | f
 distance_orderable | f
 returnable         | t
 search_array       | f
 search_nulls       | t

不支持排序,这是可预测的(因为访问方法层属性中的can_order为false)。到目前为止,SP GiST中还没有用于搜索最近邻居的距离运算符。最有可能的是,该功能将在将来得到支持。

【Nikita Glukhov即将发布的PostgreSQL 12补丁支持该功能。】

SP-GiST可用于仅索引扫描,至少用于(上述)讨论的运算符类。正如我们所看到的,在某些情况下,索引值显式存储在叶节点中,而在另一些情况下,在树下降过程中,这些值被一部分一部分地重建。

NULL

为了不让情况复杂化,我们到目前为止还没有提到空值。从索引属性可以清楚地看出,空值是受支持的:

postgres=# explain (costs off)
select * from sites where url is null;

                  QUERY PLAN                  
----------------------------------------------
 Index Only Scan using sites_url_idx on sites
   Index Cond: (url IS NULL)
(2 rows)

然而,NULL对于SP-GiST来说有时是陌生的。“spgist”运算符类中的所有运算符都必须严格:只要运算符的任何参数为NULL,就必须返回NULL。该方法本身确保了这一点:空值不会传递给运算符。

但要使用仅索引扫描的访问方法,必须在索引中存储空值。它们被储存在一棵独立的树上,有自己的根。

其他数据类型

除了点和字符串基树,PostgreSQL还实现了基于SP-GiST的其他方法:

  • “box_ops”操作符类为矩形提供了一个四叉树。每个矩形由四维空间中的一个点表示,因此象限数等于16(一个两维坐标的点将平面分成了4个象限,一个三维坐标的点将空间分成8个象限,那么一个四维坐标的点可以将四维空间分为16个象限)。当矩形有很多交叉点时,这样的索引在性能上可以超过GiST:在GiST中,不可能绘制边界以便将相交的对象彼此分开,而点(甚至是四维)则没有这样的问题(?不能理解)
  • “range_ops”操作符类为区间提供了一个四叉树。区间由二维点表示:下边界为横坐标,上边界为纵坐标。(一个区间可能由多个二维点表示,那么可以表示线段或者多边形,如果只有一个二维点,那何来的区间)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值