1. 程式人生 > 實用技巧 >PostgreSQL中的索引(七)--GIN

PostgreSQL中的索引(七)--GIN

我們已經熟悉了PostgreSQL索引引擎和訪問方法的介面,並討論了hash索引、b-trees以及GiST和SP-GiST索引。這篇文章將介紹GIN索引。

GIN

GIN是廣義倒排索引(Generalized Inverted Index)的縮寫。這就是所謂的倒排索引。它操作的資料型別的值不是原子的,而是由元素組成的。我們將這些型別稱為複合型別。索引的不是複合型別的值,而是單獨的元素;每個元素都引用它出現的值。

與此方法有一個很好的類比,即圖書末尾的索引,對於每個術語,它提供了出現該術語的頁面列表。訪問方法必須確保快速搜尋索引元素,就像書中的索引一樣。因此,這些元素被儲存為熟悉的b樹(它使用了另一種更簡單的實現,但在本例中並不重要)。 對包含元素複合值的錶行的有序引用集連結到每個元素。順序性對於資料檢索並不重要(TIDs的排序順序沒有太大意義),但對於索引的內部結構很重要。

元素永遠不會從GIN索引中刪除。我們認為,包含元素的值可以消失、出現或變化,但組成它們的元素集或多或少是穩定的。此解決方案極大地簡化了使用索引並行處理多個程序的演算法。

如果TIDs列表非常小,它可以與元素放在同一個頁面中(稱為«the posting list»)。但如果這個列表很大,就需要一個更高效的資料結構,我們已經意識到了這一點——它還是B-tree。這樣的樹位於單獨的資料頁上(稱為«the posting tree»)。

因此,GIN索引由元素的B-tree組成,而TIDs的B-tree或平面列表連結到該B-tree的葉行。

與前面討論的GiST和SP-GiST索引一樣,GIN為應用程式開發人員提供了支援複合資料型別的各種操作的介面。

全文檢索

GIN的主要應用領域是加速全文檢索,因此,在更詳細地討論該索引時,可以將其用作示例。

與GiST相關的文章已經提供了關於全文搜尋的簡單介紹,所以讓我們直接切入主題,不要重複。顯然,本例中的複合值是文件,而這些文件的元素是*lexemes。

讓我們來看看與GiST相關的文章中的例子:

postgres=# create table ts(doc text, doc_tsv tsvector);

postgres=# insert into ts(doc) values
  ('Can a sheet slitter slit sheets?'), 
  ('How many sheets could a sheet slitter slit?'),
  ('I slit a sheet, a sheet I slit.'),
  ('Upon a slitted sheet I sit.'), 
  ('Whoever slit the sheets is a good sheet slitter.'), 
  ('I am a sheet slitter.'),
  ('I slit sheets.'),
  ('I am the sleekest sheet slitter that ever slit sheets.'),
  ('She slits the sheet she sits on.');

postgres=# update ts set doc_tsv = to_tsvector(doc);

postgres=# create index on ts using gin(doc_tsv);

該索引的可能結構如圖所示:

與前面所有的圖不同,對錶行(tid)的引用是用黑色背景上的數值(頁碼和頁面上的位置)表示的,而不是用箭頭表示的。

postgres=# select ctid, left(doc,20), doc_tsv from ts;
  ctid |         left         |                         doc_tsv                         
-------+----------------------+---------------------------------------------------------
 (0,1) | Can a sheet slitter  | 'sheet':3,6 'slit':5 'slitter':4
 (0,2) | How many sheets coul | 'could':4 'mani':2 'sheet':3,6 'slit':8 'slitter':7
 (0,3) | I slit a sheet, a sh | 'sheet':4,6 'slit':2,8
 (1,1) | Upon a slitted sheet | 'sheet':4 'sit':6 'slit':3 'upon':1
 (1,2) | Whoever slit the she | 'good':7 'sheet':4,8 'slit':2 'slitter':9 'whoever':1
 (1,3) | I am a sheet slitter | 'sheet':4 'slitter':5
 (2,1) | I slit sheets.       | 'sheet':3 'slit':2
 (2,2) | I am the sleekest sh | 'ever':8 'sheet':5,10 'sleekest':4 'slit':9 'slitter':6
 (2,3) | She slits the sheet  | 'sheet':4 'sit':6 'slit':2
(9 rows)

在這個推測的示例中,所有詞素的tid列表可以位於常規頁面,但«sheet»、«slit»和«slits»除外。這些詞素出現在許多文件中,它們的tid列表已經被放置在單個的b-tree中。

順便問一下,我們如何知道一個詞素被包含在多少文件?對於一個小的表,下面所示的«direct»技術可以實現,但是我們將進一步學習如何處理較大的表。

postgres=# select (unnest(doc_tsv)).lexeme, count(*) from ts
group by 1 order by 2 desc;
  lexeme  | count 
----------+-------
 sheet    |     9
 slit     |     8
 slitter  |     5
 sit      |     2
 upon     |     1
 mani     |     1
 whoever  |     1
 sleekest |     1
 good     |     1
 could    |     1
 ever     |     1
(11 rows)

還要注意,與常規的b-樹不同,GIN索引的頁是通過單向列表連線的,而不是雙向列表。這就足夠了,因為樹遍歷只有一種方式。

查詢示例

執行以下查詢會如何執行呢?

postgres=# explain(costs off)
select doc from ts where doc_tsv @@ to_tsquery('many & slitter');
                             QUERY PLAN                              
---------------------------------------------------------------------
 Bitmap Heap Scan on ts
   Recheck Cond: (doc_tsv @@ to_tsquery('many & slitter'::text))
   ->  Bitmap Index Scan on ts_doc_tsv_idx
         Index Cond: (doc_tsv @@ to_tsquery('many & slitter'::text))
(4 rows)

單個詞素(搜尋鍵)首先從查詢中提取:«mani»和«slitter»。這是由一個專門的API函式來完成的,它考慮到由操作符類決定的資料型別和策略:

postgres=# select amop.amopopr::regoperator, amop.amopstrategy
from pg_opclass opc, pg_opfamily opf, pg_am am, pg_amop amop
where opc.opcname = 'tsvector_ops'
and opf.oid = opc.opcfamily
and am.oid = opf.opfmethod
and amop.amopfamily = opc.opcfamily
and am.amname = 'gin'
and amop.amoplefttype = opc.opcintype;
        amopopr        | amopstrategy 
-----------------------+--------------
 @@(tsvector,tsquery)  |            1  matching search query
 @@@(tsvector,tsquery) |            2  synonym for @@ (for backward compatibility)
(2 rows)

在詞素的b-樹中,我們接下來找到這兩個鍵,並遍歷tid的列表。我們得到:

對於«mani»-(0,2)。

對於«slitter»-(0,1),(0,2),(1,2),(1,3),(2,2)。

最後,對於找到的每個TID,將呼叫一個API一致性函式,該函式必須確定找到的哪一行與搜尋查詢匹配。由於我們查詢中的lexemes是由布林«and»連線的,所以返回的唯一一行是(0,2):

       |      |         |  consistency
       |      |         |    function
  TID  | mani | slitter | slit & slitter
-------+------+---------+----------------
 (0,1) |    f |       T |              f 
 (0,2) |    T |       T |              T
 (1,2) |    f |       T |              f
 (1,3) |    f |       T |              f
 (2,2) |    f |       T |              f

結果是:

postgres=# select doc from ts where doc_tsv @@ to_tsquery('many & slitter');
                     doc                     
---------------------------------------------
 How many sheets could a sheet slitter slit?
(1 row)

如果我們將這種方法與已經討論過的GiST方法進行比較,那麼GIN用於全文搜尋的優勢就很明顯了。但這裡還有比表面上看到的更多的東西。

slow update的問題

問題是GIN索引中的資料插入或更新非常慢。每個文件通常包含許多要建立索引的詞素。因此,當只新增或更新一個文件時,我們必須大規模地更新索引樹。

另一方面,如果同時更新幾個文件,它們的一些詞素可能是相同的,那麼總的工作量將比逐個更新文件時要小。

GIN索引有«fastupdate»儲存引數,我們可以在建立和更新索引時指定:

postgres=# create index on ts using gin(doc_tsv) with (fastupdate = true);

開啟此引數後,更新將在一個單獨的無序列表中累積(在各個連線的頁上)。 當這個列表足夠大或在vacuuming期間,所有累積的更新都會立即對索引進行。要考慮的列表«large enough»是由«gin_pending_list_limit»配置引數決定的,或者由索引的同名儲存引數決定的。

但是這種方法也有缺點:首先,搜尋速度變慢(因為除了樹之外還需要檢視無序列表),其次,如果無序列表已經溢位,下一次更新可能會意外地花費很多時間。

部分匹配的檢索

我們可以在全文搜尋中使用部分匹配。例如,考慮以下查詢:

gin=# select doc from ts where doc_tsv @@ to_tsquery('slit:*');
                          doc                           
--------------------------------------------------------
 Can a sheet slitter slit sheets?
 How many sheets could a sheet slitter slit?
 I slit a sheet, a sheet I slit.
 Upon a slitted sheet I sit.
 Whoever slit the sheets is a good sheet slitter.
 I am a sheet slitter.
 I slit sheets.
 I am the sleekest sheet slitter that ever slit sheets.
 She slits the sheet she sits on.
(9 rows)

這個查詢將會找到包含以«slit»開頭的詞素的文件。在這個例子中,這樣的詞素是«slit»和«slitter»。

即使沒有索引,查詢也可以正常工作,但GIN還允許加速以下搜尋:

postgres=# explain (costs off)
select doc from ts where doc_tsv @@ to_tsquery('slit:*');
                         QUERY PLAN                          
-------------------------------------------------------------
 Bitmap Heap Scan on ts
   Recheck Cond: (doc_tsv @@ to_tsquery('slit:*'::text))
   ->  Bitmap Index Scan on ts_doc_tsv_idx
         Index Cond: (doc_tsv @@ to_tsquery('slit:*'::text))
(4 rows)

這裡,所有在搜尋查詢中指定字首的詞素都在樹中查詢,並由布林«or»連線。

頻繁和不頻繁的詞素(lexemes)

為了觀察索引是如何在實時資料上工作的,讓我們以«pgsql-hacker»電子郵件的歸檔為例,我們在討論GiST時已經使用過了。這個版本的存檔包含356125條訊息,其中包含傳送日期、主題、作者和文字。

fts=# alter table mail_messages add column tsv tsvector;

fts=# update mail_messages set tsv = to_tsvector(body_plain);
NOTICE:  word is too long to be indexed
DETAIL:  Words longer than 2047 characters are ignored.
...
UPDATE 356125
fts=# create index on mail_messages using gin(tsv);

讓我們假設一個出現在許多文件中的詞素。使用«unnest»的查詢將無法在如此大的資料量上工作,正確的技術是使用«ts_stat»函式,它提供關於詞素表的資訊,它們所出現的文件數量,以及總出現次數。

fts=# select word, ndoc
from ts_stat('select tsv from mail_messages')
order by ndoc desc limit 3;
 word  |  ndoc  
-------+--------
 re    | 322141
 wrote | 231174
 use   | 176917
(3 rows)

讓我們選擇«wrote»。

我們將採用一些在開發者郵件中不常見的詞,比如«tattoo»:

fts=# select word, ndoc from ts_stat('select tsv from mail_messages') where word = 'tattoo';
  word  | ndoc 
--------+------
 tattoo |    2
(1 row)

有沒有同時出現這兩個詞的文件?似乎有:

fts=# select count(*) from mail_messages where tsv @@ to_tsquery('wrote & tattoo');
 count 
-------
     1
(1 row)

出現瞭如何執行此查詢的問題。如果我們得到兩個詞素的tid列表(如上所述),那麼搜尋顯然效率低下:我們將不得不遍歷超過20萬個值,最終只留下一個值。幸運的是,使用planner統計資料,演算法知道«wrote»經常出現,而«tatoo»很少出現。因此,執行對不常見的詞的搜尋,然後檢查檢索到的兩個文件是否有«wrote»詞。這一點在查詢中很清楚,它執行得很快:

fts=# \timing on

fts=# select count(*) from mail_messages where tsv @@ to_tsquery('wrote & tattoo');
 count 
-------
     1
(1 row)
Time: 0,959 ms

僅搜尋«wrote»就需要相當長的時間:

fts=# select count(*) from mail_messages where tsv @@ to_tsquery('wrote');
 count  
--------
 231174
(1 row)
Time: 2875,543 ms (00:02,876)

這種優化當然不僅適用於兩個詞,而且適用於更復雜的情況。

限制查詢結果

GIN訪問方法的一個特性是結果總是作為點陣圖返回:該方法不能按TID返回結果。正因為如此,本文中的所有查詢計劃都使用bitmap scan。

因此,使用LIMIT子句對索引掃描結果進行限制並不是很有效。注意操作的預測成本(«Limit»節點的«cost»欄位):

fts=# explain (costs off)
select * from mail_messages where tsv @@ to_tsquery('wrote') limit 1;
                                       QUERY PLAN
-----------------------------------------------------------------------------------------
 Limit  (cost=1283.61..1285.13 rows=1)
   ->  Bitmap Heap Scan on mail_messages  (cost=1283.61..209975.49 rows=137207)
         Recheck Cond: (tsv @@ to_tsquery('wrote'::text))
         ->  Bitmap Index Scan on mail_messages_tsv_idx  (cost=0.00..1249.30 rows=137207)
               Index Cond: (tsv @@ to_tsquery('wrote'::text))
(5 rows)

估計成本為1285.13,比構建整個點陣圖1249.30(點陣圖索引掃描節點的«cost»欄位)的成本略高。

因此,索引具有限制結果數量的特殊功能。閾值在«gin_fuzzy_search_limit»配置引數中指定,預設為零(不存在限制)。但是我們可以設定閾值:

fts=# set gin_fuzzy_search_limit = 1000;

fts=# select count(*) from mail_messages where tsv @@ to_tsquery('wrote');
 count 
-------
  5746
(1 row)
fts=# set gin_fuzzy_search_limit = 10000;

fts=# select count(*) from mail_messages where tsv @@ to_tsquery('wrote');
 count 
-------
 14726
(1 row)

我們可以看到,查詢返回的行數因引數值的不同而不同(如果使用索引訪問)。限制並不嚴格:可以返回比指定的多的行,這就證明了引數名中有«fuzzy»部分是正確的。

緊湊表示(Compact representation)

在其他方面,gin索引還是很好的,因為它們很緊湊。首先,如果一個相同的lexeme出現在多個文件中(這是通常的情況),那麼它只儲存在索引中一次。 其次,TID以有序的方式儲存在索引中,這使我們能夠使用簡單的壓縮:列表中儲存的下一個TID實際上是與前一個TID是不同點;這通常是一個很小的數字,需要比完整的6位元組TID少得多的位。

為了瞭解其大小,讓我們從訊息的文字構建B-tree。但肯定不是公平的比較:

·GIN構建在不同的資料型別上(«tsvector»而不是«text»),«tsvector»更小 ·同時,b-樹的訊息大小必須縮短到大約2kb。

我們繼續:

fts=# create index mail_messages_btree on mail_messages(substring(body_plain for 2048));

我們還將建立GiST索引:

fts=# create index mail_messages_gist on mail_messages using gist(tsv);

在«vacuum full»後索引的大小:

fts=# select pg_size_pretty(pg_relation_size('mail_messages_tsv_idx')) as gin,
             pg_size_pretty(pg_relation_size('mail_messages_gist')) as gist,
             pg_size_pretty(pg_relation_size('mail_messages_btree')) as btree;
  gin   |  gist  | btree  
--------+--------+--------
 179 MB | 125 MB | 546 MB
(1 row)

由於緊湊性,我們可以嘗試在從Oracle遷移的過程中使用GIN索引來替代點陣圖索引(為了便於理解,我提供了Lewis的文章的參考)。作為規則,點陣圖索引用於僅有少數唯一值的欄位,這對於GIN也是非常好的。並且,如第一篇文章所示,PostgreSQL可以動態地基於任何索引(包括GIN)構建點陣圖。

GiST還是GIN

對於許多資料型別,GiST和GIN都可以使用操作符類,這就產生了使用哪個索引的問題。也許,我們已經可以得出一些結論了。

通常,GIN在準確性和搜尋速度上優於GiST。如果資料更新不頻繁,並且需要快速搜尋,那麼很可能使用GIN。

另一方面,如果資料是密集更新的,那麼更新GIN的開銷可能會顯得太大。在這種情況下,我們將不得不比較這兩種選擇。

Arrays

使用GIN的另一個例子是陣列的索引。在這種情況下,陣列元素進入索引,這允許加速對陣列的一些操作:

postgres=# select amop.amopopr::regoperator, amop.amopstrategy
from pg_opclass opc, pg_opfamily opf, pg_am am, pg_amop amop
where opc.opcname = 'array_ops'
and opf.oid = opc.opcfamily
and am.oid = opf.opfmethod
and amop.amopfamily = opc.opcfamily
and am.amname = 'gin'
and amop.amoplefttype = opc.opcintype;
        amopopr        | amopstrategy 
-----------------------+--------------
 &&(anyarray,anyarray) |            1  intersection
 @>(anyarray,anyarray) |            2  contains array
 <@(anyarray,anyarray) |            3  contained in array
 =(anyarray,anyarray)  |            4  equality
(4 rows)

我們的演示資料庫有帶有航班資訊的«routes»檢視。在其他檢視中,該檢視包含«days_of_week»列——發生航班時的工作日陣列。例如,從伏努科沃到格倫齊克的航班在週二、週四和週日起飛:

demo=# select departure_airport_name, arrival_airport_name, days_of_week
from routes
where flight_no = 'PG0049';
 departure_airport_name | arrival_airport_name | days_of_week 
------------------------+----------------------+--------------
 Vnukovo                | Gelendzhik            | {2,4,7}
(1 row)

為了構建索引,讓我們將檢視“物化”到一個表中:

demo=# create table routes_t as select * from routes;

demo=# create index on routes_t using gin(days_of_week);

現在我們可以用這個索引來了解週二、週四和週日的所有航班:

demo=# explain (costs off) select * from routes_t where days_of_week = ARRAY[2,4,7];
                        QUERY PLAN                         
-----------------------------------------------------------
 Bitmap Heap Scan on routes_t
   Recheck Cond: (days_of_week = '{2,4,7}'::integer[])
   ->  Bitmap Index Scan on routes_t_days_of_week_idx
         Index Cond: (days_of_week = '{2,4,7}'::integer[])
(4 rows)

似乎有六種查詢額結果:

demo=# select flight_no, departure_airport_name, arrival_airport_name, days_of_week from routes_t where days_of_week = ARRAY[2,4,7];
 flight_no | departure_airport_name | arrival_airport_name | days_of_week 
-----------+------------------------+----------------------+--------------
 PG0005    | Domodedovo             | Pskov                | {2,4,7}
 PG0049    | Vnukovo                | Gelendzhik           | {2,4,7}
 PG0113    | Naryan-Mar             | Domodedovo           | {2,4,7}
 PG0249    | Domodedovo             | Gelendzhik           | {2,4,7}
 PG0449    | Stavropol             | Vnukovo              | {2,4,7}
 PG0540    | Barnaul                | Vnukovo              | {2,4,7}
(6 rows)

該查詢是如何執行的? 和上面描述的完全一樣:

1.從陣列{2,4,7}中提取元素(搜尋關鍵字)。顯然,這些是«2»、«4»和«7»的值。

2.在元素樹中找到提取的鍵,併為每個鍵選擇TIDs列表。

3.在找到的所有TIDs中,一致性函式從查詢中選擇與操作符匹配的TIDs。 For =操作符,只有那些tid與所有三個列表中出現的匹配(換句話說,初始陣列必須包含所有元素)。但這是不夠的:它還需要陣列不包含任何其他值,我們不能用索引檢查這個條件。 因此,在這種情況下,access method要求索引引擎重新檢查與表一起返回的所有tid。

有趣的是,有些策略(例如,«contains in array»)不能檢查任何內容,而必須重新檢查表中找到的所有tid。

但是,如果我們需要知道週二、週四和週日從莫斯科起飛的航班,該怎麼辦呢? 索引將不支援附加條件,它將進入«Filter»列。

demo=# explain (costs off)
select * from routes_t where days_of_week = ARRAY[2,4,7] and departure_city = 'Moscow';
                        QUERY PLAN                         
-----------------------------------------------------------
 Bitmap Heap Scan on routes_t
   Recheck Cond: (days_of_week = '{2,4,7}'::integer[])
   Filter: (departure_city = 'Moscow'::text)
   ->  Bitmap Index Scan on routes_t_days_of_week_idx
         Index Cond: (days_of_week = '{2,4,7}'::integer[])
(5 rows)

在這裡,這是可以的(無論如何索引只選擇6行),但如果附加條件增加了選擇能力,則需要這樣的支援。但是,我們不能僅僅建立索引:

demo=# create index on routes_t using gin(days_of_week,departure_city);
ERROR:  data type text has no default operator class for access method "gin"
HINT:  You must specify an operator class for the index or define a default operator class for the data type.

但是“btree_gin”擴充套件將提供幫助,它添加了模擬普通b樹工作的GIN操作符類。

demo=# create extension btree_gin;

demo=# create index on routes_t using gin(days_of_week,departure_city);

demo=# explain (costs off)
select * from routes_t where days_of_week = ARRAY[2,4,7] and departure_city = 'Moscow';
                             QUERY PLAN
---------------------------------------------------------------------
 Bitmap Heap Scan on routes_t
   Recheck Cond: ((days_of_week = '{2,4,7}'::integer[]) AND
                  (departure_city = 'Moscow'::text))
   ->  Bitmap Index Scan on routes_t_days_of_week_departure_city_idx
         Index Cond: ((days_of_week = '{2,4,7}'::integer[]) AND
                      (departure_city = 'Moscow'::text))
(4 rows)

JSONB

具有內建GIN支援的複合資料型別的另一個示例是JSON。為了處理JSON值,目前定義了一些操作符和函式,其中一些可以使用索引來加速:

postgres=# select opc.opcname, amop.amopopr::regoperator, amop.amopstrategy as str
from pg_opclass opc, pg_opfamily opf, pg_am am, pg_amop amop
where opc.opcname in ('jsonb_ops','jsonb_path_ops')
and opf.oid = opc.opcfamily
and am.oid = opf.opfmethod
and amop.amopfamily = opc.opcfamily
and am.amname = 'gin'
and amop.amoplefttype = opc.opcintype;
    opcname     |     amopopr      | str
----------------+------------------+-----
 jsonb_ops      | ?(jsonb,text)    |   9  top-level key exists
 jsonb_ops      | ?|(jsonb,text[]) |  10  some top-level key exists
 jsonb_ops      | ?&(jsonb,text[]) |  11  all top-level keys exist
 jsonb_ops      | @>(jsonb,jsonb)  |   7  JSON value is at top level
 jsonb_path_ops | @>(jsonb,jsonb)  |   7
(5 rows)

正如我們所看到的,有兩個操作符類可用:«jsonb_ops»和«jsonb_path_ops»。

第一個操作符類«jsonb_ops»預設使用的。所有鍵、值和陣列元素都作為初始JSON文件的元素到達索引。每個元素都添加了一個屬性,它指示該元素是否為鍵(這是«exists»策略所需要的,它區分鍵和值)。

例如,讓我們用JSON表示«routes»中的幾行:

demo=# create table routes_jsonb as
  select to_jsonb(t) route 
  from (
      select departure_airport_name, arrival_airport_name, days_of_week
      from routes 
      order by flight_no limit 4
  ) t;

demo=# select ctid, jsonb_pretty(route) from routes_jsonb;
 ctid  |                 jsonb_pretty                  
-------+-------------------------------------------------
 (0,1) | {                                              +
       |     "days_of_week": [                          +
       |         1                                      +
       |     ],                                         +
       |     "arrival_airport_name": "Surgut",          +
       |     "departure_airport_name": "Ust-Ilimsk"     +
       | }
 (0,2) | {                                              +
       |     "days_of_week": [                          +
       |         2                                      +
       |     ],                                         +
       |     "arrival_airport_name": "Ust-Ilimsk",      +
       |     "departure_airport_name": "Surgut"         +
       | }
 (0,3) | {                                              +
       |     "days_of_week": [                          +
       |         1,                                     +
       |         4                                      +
       |     ],                                         +
       |     "arrival_airport_name": "Sochi",           +
       |     "departure_airport_name": "Ivanovo-Yuzhnyi"+
       | }
 (0,4) | {                                              +
       |     "days_of_week": [                          +
       |         2,                                     +
       |         5                                      +
       |     ],                                         +
       |     "arrival_airport_name": "Ivanovo-Yuzhnyi", +
       |     "departure_airport_name": "Sochi"          +
       | }
(4 rows)

demo=# create index on routes_jsonb using gin(route);

索引看起來如下:

現在,像這樣的查詢,例如,可以使用索引執行:

demo=# explain (costs off) 
select jsonb_pretty(route) 
from routes_jsonb 
where route @> '{"days_of_week": [5]}';
                          QUERY PLAN                           
---------------------------------------------------------------
 Bitmap Heap Scan on routes_jsonb
   Recheck Cond: (route @> '{"days_of_week": [5]}'::jsonb)
   ->  Bitmap Index Scan on routes_jsonb_route_idx
         Index Cond: (route @> '{"days_of_week": [5]}'::jsonb)
(4 rows)

從JSON文件的根開始,@>操作符檢查指定的路由(“days_of_week”:[5])是否出現。這裡查詢將返回一行:

demo=# select jsonb_pretty(route) from routes_jsonb where route @> '{"days_of_week": [5]}';
                 jsonb_pretty                 
------------------------------------------------
 {                                             +
     "days_of_week": [                         +
         2,                                    +
         5                                     +
     ],                                        +
     "arrival_airport_name": "Ivanovo-Yuzhnyi",+
     "departure_airport_name": "Sochi"         +
 }
(1 row)

查詢執行如下:

1.在搜尋查詢(“days_of_week”:[5])中提取元素(搜尋鍵):«days_of_week»和«5»。

2.在元素樹中找到提取的鍵,併為每個鍵選擇tid列表:對於«5»-(0,4),對於«days_of_week»-(0,1),(0,2),(0,3),(0,4)。

3.在找到的所有TIDs中,一致性函式從查詢中選擇與操作符匹配的TIDs。 對於@>操作符,不包含來自搜尋查詢的所有元素的文件將不能確定,因此只剩下(0,4)。 但是我們仍然需要重新檢查表中剩下的TID,因為從索引中不清楚找到的元素在JSON文件中出現的順序。

要了解其他操作符的更多細節,可以閱讀文件。

除了處理JSON的傳統操作外,«jsquery»擴充套件早就可用了,它定義了一種功能更豐富的查詢語言(當然,還支援GIN索引)。此外,2016年釋出了新的SQL標準,定義了自己的一套操作和查詢語言«SQL/JSON path»。這個標準的實現已經完成,我們相信它會出現在PostgreSQL 11中。

內部原理

我們可以使用“pageinspect”擴充套件檢視GIN索引內部。

fts=# create extension pageinspect;

來自meta頁面的資訊顯示了一般的統計資料:

fts=# select * from gin_metapage_info(get_raw_page('mail_messages_tsv_idx',0));
-[ RECORD 1 ]----+-----------
pending_head     | 4294967295
pending_tail     | 4294967295
tail_free_size   | 0
n_pending_pages  | 0
n_pending_tuples | 0
n_total_pages    | 22968
n_entry_pages    | 13751
n_data_pages     | 9216
n_entries        | 1423598
version          | 2

頁面結構提供了訪問方法(access method)儲存其資訊的特殊區域;這個區域對於像vacuum這樣的普通程式是«opaque»的。«gin_page_opaque_info»函式顯示了GIN的資料。例如,我們可以瞭解到索引頁的集合:

fts=# select flags, count(*)
from generate_series(1,22967) as g(id), -- n_total_pages
     gin_page_opaque_info(get_raw_page('mail_messages_tsv_idx',g.id))
group by flags;
         flags          | count 
------------------------+-------
 {meta}                 |     1  meta page
 {}                     |   133  internal page of element B-tree
 {leaf}                 | 13618  leaf page of element B-tree
 {data}                 |  1497  internal page of TID B-tree
 {data,leaf,compressed} |  7719  leaf page of TID B-tree
(5 rows)

«gin_leafpage_items»函式提供儲存在page {data,leaf,compressed}的tid資訊:

fts=# select * from gin_leafpage_items(get_raw_page('mail_messages_tsv_idx',2672));
-[ RECORD 1 ]---------------------------------------------------------------------
first_tid | (239,44)
nbytes    | 248
tids      | {"(239,44)","(239,47)","(239,48)","(239,50)","(239,52)","(240,3)",...
-[ RECORD 2 ]---------------------------------------------------------------------
first_tid | (247,40)
nbytes    | 248
tids      | {"(247,40)","(247,41)","(247,44)","(247,45)","(247,46)","(248,2)",...
...

這裡請注意,TIDs樹的leave頁面實際上包含指向錶行的經過壓縮的小指標列表,而不是單個指標。

屬性

讓我們看看GIN access method的屬性

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

有趣的是,GIN支援建立多列索引。但是,與常規b-樹不同的是,多列索引將仍然儲存單個元素,並且將為每個元素標明列號。

以下索引層屬性可用:

     name      | pg_index_has_property 
---------------+-----------------------
 clusterable   | f
 index_scan    | f
 bitmap_scan   | t
 backward_scan | f

注意,不支援按TID(索引掃描)返回結果;只能進行點陣圖掃描。

也不支援Backward掃描:該特性對index-scan only掃描至關重要,但對點陣圖掃描不支援。

列層屬性如下:

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

這裡沒有可用的內容:沒有排序(這很明顯),沒有使用索引作為覆蓋(因為文件本身沒有儲存在索引中),沒有空值操作(因為它對複合型別的元素沒有意義)。

其他資料型別

還有一些擴充套件為某些資料型別添加了對GIN的支援。

·“pg_trgm”使我們能夠通過比較有多少相等的三字母序列(三元組合)可用來確定單詞的«likeness»。添加了兩個操作符類«gist_trgm_ops»和«gin_trgm_ops»,它們支援各種操作符,包括通過LIKE和正則表示式進行比較。我們可以將此擴充套件與全文搜尋一起使用,以便建議糾正拼寫錯誤的單詞選項。 ·“hstore”實現«key-value»儲存。對於該資料型別,可以使用用於各種訪問方法的操作符類,包括GIN。然而,隨著«jsonb»資料型別的引入,也就沒有使用«hstore»的理由了。 ·“intarray”擴充套件了整數陣列的功能。索引支援包括GiST以及GIN(«gin__int_ops» 操作符類)。

這兩個擴充套件已經在上面提到過:

·“btree_gin”添加了對常規資料型別的GIN支援,以便它們可以與複合型別一起在多列索引中使用。 ·“jsquery”定義了一種用於JSON查詢的語言和一個用於支援該語言的索引的操作符類。 這個擴充套件不包括在標準的PostgreSQL交付中。

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