日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 运维知识 > 数据库 >内容正文

数据库

PostgreSQL中的索引—7(GIN)

發布時間:2023/12/20 数据库 27 豆豆
生活随笔 收集整理的這篇文章主要介紹了 PostgreSQL中的索引—7(GIN) 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

目錄

GIN

一般概念

全文檢索

查詢示例

更新緩慢的問題

部分匹配的搜索

頻繁詞和不頻繁詞

限制查詢結果

緊湊表示

GiST還是GIN?

數組

JSONB

內部構件

屬性

其他數據類型


我們已經熟悉了PostgreSQL索引引擎和訪問方法的接口,并討論了哈希索引、B樹以及GiST和SP GiST索引。本文將以GIN索引為特色。

GIN

“Gin?..Gin似乎是一種美國酒..”

“我不是酒鬼,哦,好奇的孩子!”老人又一次勃然大怒,他又一次意識到自己,又一次控制住自己。“我不是一種飲料,而是一種強大而無畏的精神,世界上沒有我做不到的魔法。”

— Lazar Lagin,“老霍塔比奇”

Gin代表廣義倒排索引,應該被視為精靈,而不是飲料。——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)

查詢操作如下:

  • 在搜索查詢("days_of_week": [5])中,這些元素(搜索鍵)被提取:"days_of_week"?和“5”。
  • 在元素樹中找到提取的鍵,并為每個元素選擇TID列表:對于“5”-(0,4),對于“days_of_week”-(0,1)、(0,2)、(0,3)、(0,4)。
  • 在找到的所有TID中,一致性函數從查詢中選擇與運算符匹配的TID。對于@>運算符,不包含搜索查詢中所有元素的文檔將無法留下,因此只剩下(0,4)。但我們仍然需要重新檢查表中留下的TID,因為從索引中不清楚找到的元素在JSON文檔中出現的順序(即,不知道搜索關鍵詞在目標文檔內的哪里,所以需要全文檢查目標TID指向的文檔,以高亮地顯示關鍵詞,下篇文章中提到的RUM索引會存儲關鍵字在文檔中的位置,就不需要重新檢查了)
  • 要了解其他操作員的更多詳細信息,可以閱讀文檔

    除了處理JSON的常規操作外,“jsquery”擴展長期可用,它定義了一種具有更豐富功能的查詢語言(當然,還支持GIN索引)。此外,2016年發布了新的SQL標準,該標準定義了自己的一組操作和查詢語言“SQL/JSON path”。該標準的實現已經完成,我們相信它將出現在PostgreSQL 11中。

    【SQL/JSON path補丁最終提交給PostgreSQL 12,而其他部分仍在進行中。希望我們能在PostgreSQL 13中看到完全實現的功能。】

    內部構件

    我們可以使用“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可以對這部分數據進行操作的意思吧)。“gin_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)的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。