我們已經熟悉了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)
查詢示例
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。
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的開銷可能會顯得太大。在這種情況下,我們將不得不比較這兩種選擇。
使用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)
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交付中。