PostgreSQL中的索引—7(GIN)
目錄
GIN
一般概念
全文檢索
查詢示例
更新緩慢的問題
部分匹配的搜索
頻繁詞和不頻繁詞
限制查詢結果
緊湊表示
GiST還是GIN?
數組
JSONB
內部構件
屬性
其他數據類型
我們已經熟悉了PostgreSQL索引引擎和訪問方法的接口,并討論了哈希索引、B樹以及GiST和SP GiST索引。本文將以GIN索引為特色。
GIN
“Gin?..Gin似乎是一種美國酒..”
“我不是酒鬼,哦,好奇的孩子!”老人又一次勃然大怒,他又一次意識到自己,又一次控制住自己?!拔也皇且环N飲料,而是一種強大而無畏的精神,世界上沒有我做不到的魔法。”
— Lazar Lagin,“老霍塔比奇”
Gin代表廣義倒排索引,應該被視為精靈,而不是飲料?!?span style="color:#956fe7;">README
一般概念
GIN是縮寫的廣義倒排索引(Generalized Inverted Index)。這就是所謂的倒排索引(倒排索引的概念可以參考這篇博文:什么是倒排索引?_starzhou的專欄-CSDN博客_什么是倒排索引)。它處理的數據類型的值不是原子的,而是由元素組成的。我們將這些類型稱為復合型。這些不是索引的值,而是單個元素;每個元素都引用它出現的值。
與此方法的一個很好的類比是書末的索引,它為每個術語提供了一個出現此術語的頁面列表。訪問方法必須確保索引元素的快速搜索(就是能根據術語快速找到它出現的位置,而不是遍歷每頁的關鍵詞,找到出現的頁),就像書中的索引一樣。因此,這些元素被存儲為一個熟悉的B樹(它使用了一個不同的、更簡單的實現,但在這種情況下并不重要)。對包含元素復合值的表行的有序引用集鏈接到每個元素。有序性對于數據檢索來說并不重要(TID的排序順序意義不大),但對于索引的內部結構來說很重要。(后文提到了,這樣后一頁可以存儲與前一頁的差異就行)
元素永遠不會從索引中刪除。人們認為,包含元素的值可以消失、出現或變化,但它們所組成的元素集或多或少是穩定的。這個解決方案大大簡化了多個進程并發工作的算法。
如果TID列表非常小,它可以與元素放在同一個頁面中(稱為“發布列表”)。但是如果列表很大,就需要一個更高效的數據結構,我們已經意識到了這一點——它又是B樹。這種樹位于不同的數據頁上(稱為“發布樹”)。
因此,GIN索引由元素的B樹組成,TID的B樹或平面列表鏈接到該B樹的葉行。
就像前面討論的GiST和SP-GiST索引一樣,GIN為應用程序開發人員提供了支持復合數據類型上各種操作的接口。
全文檢索
GIN方法的主要應用領域是加速全文搜索,因此,在對該索引進行更詳細的討論時,可以將其用作示例。
這篇與GiST相關的文章已經對全文搜索做了一個小的介紹,所以讓我們直接切入主題,不要重復。很明顯,本例中的復合值是文檔,而這些文檔的元素是詞素。
讓我們將前面GIST相關文章中的例子改為GIN索引:
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”和“slitter”,這些詞素出現在許多文檔中,它們的TID列表被放在單個B-樹中。
順便問一下,我們如何計算出有多少文檔包含指定詞素?對于一個小表,下面顯示的“直接”技術將起作用,但我們將進一步了解如何處理較大的表(就是后面使用ts_stat方法)。
postgres=# select (unnest(doc_tsv)).lexeme, count(*) from ts group by 1 order by 2 desc;lexeme | count ----------+-------sheet | 9slit | 8slitter | 5sit | 2upon | 1mani | 1whoever | 1sleekest | 1good | 1could | 1ever | 1 (11 rows)還要注意的是,與常規的B-樹不同,GIN索引的頁面是通過單向列表而不是雙向列表連接的。這就足夠了,因為樹遍歷只有一種方式。(因為不會有將包含某一詞素的文檔按TID存儲順序輸出的需求,只會按相關性排序輸出需求)
查詢示例
對于我們的示例,下面的查詢將如何執行?
postgres=# explain(costs off) select doc from ts where doc_tsv @@ to_tsquery('many & slitter');---------------------------------------------------------------------Bitmap Heap Scan on tsRecheck Cond: (doc_tsv @@ to_tsquery('many & slitter'::text))-> Bitmap Index Scan on ts_doc_tsv_idxIndex 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一致性函數,該函數必須確定找到的那些行與搜索查詢匹配。由于查詢中的詞素由布爾“and”連接,因此返回的唯一一行是(0,2):
| | | consistency| | | functionTID | 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用于全文搜索的優勢就顯而易見了。但這其中還有更多東西是看不見的。
更新緩慢的問題
問題是,GIN索引中的數據插入或更新非常緩慢。每個文檔通常包含許多要索引的詞素。因此,當只添加或更新一個文檔時,我們必須大量更新索引樹。
另一方面,如果同時更新多個文檔,它們的某些詞素可能是相同的,并且總工作量將小于逐個更新文檔時的工作量。
GIN索引有“fastupdate”存儲參數,我們可以在創建索引和以后更新時指定該參數:
postgres=# create index on ts using gin(doc_tsv) with (fastupdate = true);啟用此參數后,更新將累積在一個單獨的無序列表中(在各個連接的頁面上)。當這個列表足夠大或在清空過程中,所有累積的更新都會立即對索引進行更新。列表是否足夠大是由“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 tsRecheck Cond: (doc_tsv @@ to_tsquery('slit:*'::text))-> Bitmap Index Scan on ts_doc_tsv_idxIndex Cond: (doc_tsv @@ to_tsquery('slit:*'::text)) (4 rows)在這里,所有具有搜索查詢中指定前綴的詞素都會在樹中查找,并通過布爾“or”連接。(而且看上面的示例圖,索引中的詞素在B樹中,是有序的)
頻繁詞和不頻繁詞
為了觀察索引如何在實時數據上工作,讓我們看一下“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 356125fts=# 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 | 322141wrote | 231174use | 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多萬個值(就是在出現wrote這個詞的20多萬封郵件中逐個與出現tatoo的文檔比較TID值),只剩下其中一個。幸運的是,使用planner統計數據,該算法了解到“writed”詞素經常出現,而“tatoo”很少出現。因此,執行對不常見詞素的搜索,然后檢查檢索到的兩個文檔是否存在“已寫”詞素。從快速執行的查詢中可以清楚地看到:
fts=# \timing onfts=# 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返回結果。正因為如此,本文中的所有查詢計劃都使用位圖掃描。
因此,使用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(“Bitmap Index Scan”字段)的成本。
因此,該索引使用特殊手段來限制結果數量。閾值在“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)如我們所見,查詢返回的行數因參數值不同而不同(如果使用索引訪問)。限制并不嚴格:可以返回比指定的行更多的行,這證明了參數名的“模糊”部分是正確的。
緊湊表示
在其他方面,GIN由于其緊湊性表現很好。首先,如果同一個詞素出現在多個文檔中(通常是這樣),那么它只在索引中存儲一次。第二,TID以有序的方式存儲在索引中,這使我們能夠使用簡單的壓縮:列表中的每個下一個TID實際上都存儲為其與上一個TID的差異;這通常是一個小數字,需要的位比完整的六字節TID少得多。
為了了解(GIN、GiST與B樹索引對于相同內容創建的索引)大小,讓我們從(郵件的)信息文本中構建B樹。但公平的比較(前提)肯定不會發生:
- GIN基于不同的數據類型(“tsvector”而非“文本”)構建,后者更小,
- 同時,B-tree的信息大小必須縮短到大約2KB。
然而,我們繼續(默認使用的是B樹索引):
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的間接成本可能會顯得太大。在這種情況下,我們將不得不比較這兩個選項,并選擇一個其特點更好地平衡。
數組
使用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)我們的演示數據庫有“航線”視圖,其中包含航班信息。在其余的視圖中,該視圖包含“days_of_week”列,這是一個航班在工作日起飛的數組。例如,從Vnukovo飛往Gelendzhik的航班在周二、周四和周日起飛:
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_tRecheck Cond: (days_of_week = '{2,4,7}'::integer[])-> Bitmap Index Scan on routes_t_days_of_week_idxIndex 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)這個查詢是如何執行的?其實與上面的方法完全相同:
- 從在這里扮演搜索查詢角色的數組{2,4,7}中,提取元素(搜索關鍵字)。顯然,這些是“2”、“4”和“7”的值。
- 在樹的元素中,會找到提取的鍵,并為每個鍵選擇TID列表。
- 在找到的所有TID中,一致性函數從查詢中選擇與運算符匹配的TID。對于=運算符,只有三個列表中出現的那些TID與之匹配(換句話說,初始數組必須包含所有元素)。但這還不夠:數組還需要不包含任何其他值,我們不能用索引檢查這個條件。因此,在這種情況下,訪問方法要求索引引擎重新檢查隨表返回的所有TID。
有趣的是,有些策略(例如,“包含在數組中”)無法檢查任何內容,必須重新檢查表中找到的所有TID。
但如果我們需要知道周二、周四和周日從莫斯科起飛的航班,該怎么辦?索引將不支持附加條件,它將進入“過濾器”列。
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_tRecheck Cond: (days_of_week = '{2,4,7}'::integer[])Filter: (departure_city = 'Moscow'::text)-> Bitmap Index Scan on routes_t_days_of_week_idxIndex Cond: (days_of_week = '{2,4,7}'::integer[]) (5 rows)在這里這是可以的(索引無論如何只選擇六行,行數很少),但是在附加條件增加選擇能力的情況下,希望有這樣的支持。然而,我們不能僅僅創建索引:
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_tRecheck Cond: ((days_of_week = '{2,4,7}'::integer[]) AND(departure_city = 'Moscow'::text))-> Bitmap Index Scan on routes_t_days_of_week_departure_city_idxIndex 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 existsjsonb_ops | ?|(jsonb,text[]) | 10 some top-level key existsjsonb_ops | ?&(jsonb,text[]) | 11 all top-level keys existjsonb_ops | @>(jsonb,jsonb) | 7 JSON value is at top leveljsonb_path_ops | @>(jsonb,jsonb) | 7 (5 rows)我們可以看到,有兩個操作符類可用:“jsonb_ops”和“jsonb_path_ops”。
默認情況下使用第一個操作符類“jsonb_ops”。所有鍵、值和數組元素都作為初始JSON文檔的元素掛載在索引上。每個元素都添加了一個屬性,該屬性指示該元素是否為鍵(區分鍵和值的“exists”策略需要該屬性)。
例如,讓我們將“routes”中的幾行表示為JSON,如下所示:
demo=# create table routes_jsonb asselect to_jsonb(t) route from (select departure_airport_name, arrival_airport_name, days_of_weekfrom 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_jsonbRecheck Cond: (route @> '{"days_of_week": [5]}'::jsonb)-> Bitmap Index Scan on routes_jsonb_route_idxIndex 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)查詢操作如下:
要了解其他操作員的更多詳細信息,可以閱讀文檔。
除了處理JSON的常規操作外,“jsquery”擴展長期可用,它定義了一種具有更豐富功能的查詢語言(當然,還支持GIN索引)。此外,2016年發布了新的SQL標準,該標準定義了自己的一組操作和查詢語言“SQL/JSON path”。該標準的實現已經完成,我們相信它將出現在PostgreSQL 11中。
【SQL/JSON path補丁最終提交給PostgreSQL 12,而其他部分仍在進行中。希望我們能在PostgreSQL 13中看到完全實現的功能?!?/p>
內部構件
我們可以使用“pageinspect”擴展來查看內部索引。
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頁面結構提供了一個特殊區域,訪問方法在其中存儲信息;這個區域對于vacuum等普通程序來說是“不透明的”(就是vacuum可以對這部分數據進行操作的意思吧)?!癵in_page_opaque_info”函數顯示GIN索引的這些數據。例如,我們可以了解索引頁的集合:
fts=# select flags, count(*) from generate_series(1,22967) as g(id), -- n_total_pagesgin_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”函數提供有關存儲在頁面上的TID的信息{data,leaf,compressed}:
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)",... ...請注意,TID樹的葉頁實際上包含指向表行的指針的小壓縮列表,而不是單個指針。
屬性
讓我們看看GIN訪問方法的屬性。
amname | name | pg_indexam_has_property --------+---------------+-------------------------gin | can_order | fgin | can_unique | fgin | can_multi_col | tgin | can_exclude | f有趣的是,GIN支持創建多列索引。然而,與常規B-樹不同的是,多列索引仍將存儲單個元素,并為每個元素指示列號,而不是復合鍵。(上面已經有JSONB的例子了,但是也要看字段的數據類型,比如航班的數組例子中,雖然內容一樣,但是飛機的起止點作為text類型存的話,就需要擴展才能建多列索引)
以下為索引層的特性:
name | pg_index_has_property ---------------+-----------------------clusterable | findex_scan | fbitmap_scan | tbackward_scan | f請注意,索引掃描不支持按TID逐個返回結果,而位圖掃描可以。
反向掃描也不受支持:此功能僅對索引掃描是必需的,但對位圖掃描不是必需的。
以下是列層面的特性:
name | pg_index_column_has_property --------------------+------------------------------asc | fdesc | fnulls_first | fnulls_last | forderable | fdistance_orderable | freturnable | fsearch_array | fsearch_nulls | f這里沒有可用的特性:不能排序(這很清楚,因為檢索到的文檔排序沒有意義),不能使用索引作為覆蓋(因為文檔本身不存儲在索引中),沒有對空值的操作(因為它對復合類型的元素沒有意義,你不能去搜索一個文檔是否包含NULL)。
其他數據類型
還有一些擴展可以為某些數據類型添加對GIN的支持。
- “pg_trgm”使我們能夠通過比較有多少相等的三字母序列(三叉圖)來確定單詞的“相似性”。增加了兩個操作符類,“gist_trgm_ops”和“gin_trgm_ops”,它們支持各種操作符,包括通過LIKE和正則表達式進行比較。我們可以將此擴展與全文搜索結合使用,以建議修復打字錯誤的單詞選項。
- “hstore”實現“鍵值”存儲。對于這種數據類型,可以使用各種訪問方法的運算符類,包括GIN。然而,隨著“jsonb”數據類型的引入,并沒有什么特別的理由需要使用“hstore”。
- “intarray”擴展了整數數組的功能。索引支持包括GiST和GIN(“gin_int_ops”操作符類)。
以及上面已經提到了這兩個擴展:
- “btree_gin”增加了對常規數據類型的gin支持,以便在多列索引中與復合類型一起使用。
- “jsquery”定義了一種用于JSON查詢的語言,以及一個用于該語言索引支持的運算符類。標準PostgreSQL交付中不包括此擴展。
總結
以上是生活随笔為你收集整理的PostgreSQL中的索引—7(GIN)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: RSA PKCS1和PKCS8的ASN格
- 下一篇: PBX 电话