PostgreSQL中的索引(六) --(SP-GiST)


我們已經討論過PostgreSQL索引引擎,訪問方法的接口,以及三種方法:hash index, B-tree和GiST。在本文中,我們將描述SP-GiST。

SP-GiST

首先,簡單介紹一下這個名字。«GiST»部分暗示了同GiST訪問方法的一些相似性。相似性確實存在:兩者都是廣義搜索樹,為構建各種訪問方法提供了框架。

«SP»代表空間分區。這里的空間通常就是我們所說的空間,例如,一個二維平面。但我們會發現,任何搜索空間,實際上都是任意值域。

SP-GiST適用於可以遞歸地將空間分割為不相交區域的結構。這個類包括四叉樹、k維樹(k-D樹)和基數樹(radix trees)

Structure

因此,SP-GiST訪問方法的思想是將值域(value domain)分割為不重疊的子域,每個子域依次也可以分割。這樣的划分導致了樹的不平衡(不像b樹和常規的GiST)。

不相交的特性簡化了在插入和搜索時的決策。另一方面,作為規則,樹是低分枝的。例如,四叉樹的一個節點通常有四個子節點(與b樹不同,b樹的節點有數百個),而且深度更大。像這樣的樹很適合在RAM中工作,但索引存儲在磁盤上,因此,為了減少I/O操作的數量,必須將節點打包到頁中,而高效地做到這一點並不容易。此外,由於分支深度的不同,在索引中找到不同值所需的時間也會不同。

這種訪問方法與GiST的方式相同,它處理低級別任務(同時訪問和鎖定、日志記錄和純搜索算法),並提供專門的簡化接口,以支持添加對新數據類型和新分區算法的支持。

SP-GiST樹的內部節點存儲對子節點的引用;可以為每個引用定義一個標簽。 此外,一個內部節點可以存儲一個稱為前綴的值。實際上,這個值不是必須的前綴;它可以看作是滿足所有子節點的任意謂詞。

SP-GiST的葉子節點包含索引類型的值和對表行(TID)的引用。被索引的數據本身(搜索鍵)可以用作值,但不是強制性的:可以存儲一個縮短的值。

此外,葉子節點可以分組到列表中。因此,內部節點不僅可以引用一個值,還可以引用整個列表。

請注意,葉節點中的前綴、標簽和值具有各自獨立的數據類型。

與GiST相同,定義搜索的主要函數是一致性函數。對樹節點調用該函數,並返回一組子節點,其值«是一致的»與搜索謂詞(通常以“indexed-field operator expression”的形式)。對於葉節點,一致性函數確定該節點中的索引值是否滿足搜索謂詞。

搜索從根節點開始。一致性函數找出訪問哪些子節點是有意義的。算法對每個找到的節點重復執行。搜索是深度優先的。

在物理層,索引節點被打包到頁(page)中,以便從I/O操作的角度有效地使用節點。請注意,一個頁面可以包含內部節點或葉節點,但不能同時包含這兩種節點。

四叉樹示例

四叉樹用於索引平面上的點。一個想法是遞歸地將區域分割成相對於中心點的四個部分(象限)。這種樹中分支的深度可以變化,並取決於適當象限中點的密度。

這是由openflights.org站點的機場擴展的演示數據庫的示例,如圖所示。順便說一下,最近我們發布了一個新版本的數據庫,其中我們用«point»類型的字段替換了經度和緯度。

 

首先,我們分成四個象限。

然后我們把每個象限分開

以此類推,直到我們得到最終的划分。

讓我們來看在與gist相關的文章中已經考慮過的一個簡單示例的更多細節。看看這種情況下的分區是什么樣子的:

 

象限編號如圖1所示。為了明確起見,讓我們將子節點從左到右完全按照相同的順序排列。在這種情況下,可能的索引結構如下圖所示。每個內部節點最多引用4個子節點。每個引用都可以用象限號標記,如圖所示。但是在實現中沒有標簽,因為存儲一個包含四個引用的固定數組更方便,其中一些引用可以是空的。

位於邊界上的點與數值較小的象限有關。

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數據庫為例,它用於用世界地圖繪制圖片。

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-dimensional(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 |
 ...

radix樹示例

我們還可以使用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樹的操作符:«equal»、«greater»和«less»:

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»,因此索引方法將返回兩個值:«postgrespro.com»和«postgrespro.ru»。在過濾階段將從它們中選擇一個匹配值。

 

原理

讓我們看看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_has_property
---------------+-----------------------
 clusterable   | f
 index_scan    | t
 bitmap_scan   | t
 backward_scan | f

這里與GiST的區別是clusterable不支持。

最后是列層的屬性:

        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

不支持排序,這是可預測的。到目前為止,SP-GiST中還沒有用於搜索最近鄰居的距離運算符。最有可能的是,這個特性將來會得到支持。

即將發布的由Nikita Glukhov發布的PostgreSQL 12補丁將支持它。

SP-GiST可用於index-only掃描,至少用於討論過的操作符類。正如我們所看到的,在某些情況下,索引值顯式存儲在葉節點中,而在其他情況下,在樹下降過程中,將部分地重新構建這些值。

NULLs

為了不使問題復雜化,我們到目前為止還沒有提到nulls。從索引屬性可以清楚地看出,支持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對於spgist來說是陌生的。來自«spgist»操作符類的所有操作符必須是嚴格的:當操作符的任何參數為空時,都必須返回NULL。方法本身確保了這一點:null只是沒有傳遞給操作符。

但是要使用僅索引掃描的訪問方法,無論如何都必須將空值存儲在索引中。它們被存儲在一個單獨的樹中,有自己的根。

其他數據類型

除了點和用於字符串的基數樹,其他基於SP-GiST的方法也在PostgreSQL中實現:

·«box_ops»操作符類為矩形提供了一個四叉樹。 每個矩形由四維空間中的一個點表示,因此象限的數目等於16。 當矩形有很多交點時,這樣的索引可以在性能上擊敗GiST:在GiST中不可能畫出邊界來將相交的對象從另一個分離,而點則沒有這樣的問題(甚至四維)。 ·«range_ops»操作符類為intervals提供了一個四叉樹。 區間用二維點表示,下邊界為橫坐標,上邊界為縱坐標。

 

原文地址:https://habr.com/en/company/postgrespro/blog/446624/


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM