概觀#
依作者的說法,GIN 代表的是「強大而無畏的精神」(a potent and undaunted spirit),而不是那種酒。不過它也有正式的解讀:Generalized Inverted Index(廣義倒排索引)。
GIN 存取方法是為由分離元素組成的非原子值設計的資料型別而生(例如全文檢索脈絡下,文件由詞位 lexeme 組成)。
這方法可類比書末的索引:它收錄所有重要詞條,並列出這些詞條被提及的所有頁碼。為了好用,它必須依字母序編排,否則無從快速查找。同理,GIN 仰賴「複合值的所有元素皆可排序」這件事,其主要資料結構是 B-tree。
兩項重要推論#
GIN 的元素樹實作比一般 B-tree 簡單,因為它被設計來容納重複多次的小集合元素。這個假設帶來兩個結論:
一、元素在索引中只需存一次。
每個元素被對映到一份 TID 清單,稱為 posting list:
- 清單較短時,與元素存放在一起
- 清單較長時,被搬進獨立的 posting tree(本質上也是一棵 B-tree)
與元素樹一樣,posting list 也是有序的。這對使用者來說差別不大,但有助於加速資料存取並縮小索引體積。
二、沒有必要從樹中移除元素。
即使某元素的 TID 清單已空,同一個元素很可能會再度出現在其他值中。
因此,索引就是一棵元素樹,其葉項目繫結到「扁平的 TID 清單」或「TID 樹」。
與 GiST、SP-GiST 一樣,GIN 可透過簡化的運算子類別介面索引各種資料型別。要索引特定資料型別,GIN 必須能夠:把複合值拆成元素、排序這些元素、檢查找到的值是否滿足查詢——這些操作都由運算子類別的支援函式實作。
全文檢索索引#
GIN 主要用於加速全文檢索。此處的複合值是文件,元素則是詞位。
=> CREATE INDEX ts_gin_idx ON ts USING gin(doc_tsv);延伸:範例資料表「Old MacDonald」
=> SELECT ctid, * FROM ts;
ctid | doc | doc_tsv
−−−−−−−+−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−+−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−
(0,1) | Old MacDonald had a farm | 'farm':5 'macdonald':2 'old':1
(0,2) | And on his farm he had some cows | 'cow':8 'farm':4
(0,3) | Here a moo, there a moo | 'moo':3,6
(0,4) | Everywhere a moo moo | 'everywher':1 'moo':3,4
(1,1) | Old MacDonald had a farm | 'farm':5 'macdonald':2 'old':1
(1,2) | And on his farm he had some chicks | 'chick':8 'farm':4
(1,3) | Here a cluck, there a cluck | 'cluck':3,6
(1,4) | Everywhere a cluck cluck | 'cluck':3,4 'everywher':1
(2,1) | Old MacDonald had a farm | 'farm':5 'macdonald':2 'old':1
(2,2) | And on his farm he had some pigs | 'farm':4 'pig':8
(2,3) | Here an oink, there an oink | 'oink':3,6
(2,4) | Everywhere an oink oink | 'everywher':1 'oink':3,4
(12 rows)在這個理論範例中,所有 posting list 都塞得進一般頁面,只有 farm 詞位例外——它出現在多達六份文件中,因此它的 TID 被搬進獨立的 posting tree。

圖 28-1:GIN 索引的可能結構——詞位樹的葉項目繫結到 posting list,`farm` 的 TID 則移入獨立的 posting tree

圖 28-2:`farm` 詞位的 posting tree
與一般 B-tree 的差異#
| 面向 | 一般 B-tree | GIN |
|---|---|---|
| 內部節點最左側的鍵 | 為空(實際上冗餘) | 根本不儲存,指向子節點的參照也隨之位移 |
| high key | 有 | 有,且位於正當的最右側位置 |
| 同層節點的串接 | 雙向串列 | 單向串列(樹永遠只朝一個方向走訪) |
頁面佈局#
GIN 的頁面佈局與 B-tree 非常相似,可用 pageinspect 擴充窺看。第零頁(metapage)含有基本統計,例如元素數量與各類頁面數:
=> SELECT *
FROM gin_metapage_info(get_raw_page('mail_gin_idx',0)) \gx
−[ RECORD 1 ]−−−−+−−−−−−−−−−−
pending_head | 4294967295
pending_tail | 4294967295
tail_free_size | 0
n_pending_pages | 0
n_pending_tuples | 0
n_total_pages | 22957
n_entry_pages | 13522
n_data_pages | 9434
n_entries | 999109
version | 2GIN 使用索引頁面的特殊空間(special space),其中存放定義頁面類型的位元:
flags | count
−−−−−−−−−−−−−−−−−−−−−−−−+−−−−−−−
{meta} | 1
{} | 137
{data} | 1525
{data,leaf,compressed} | 7909
{leaf} | 13385
(5 rows)- 帶
meta屬性者即 metapage - 帶
data屬性的頁面屬於 posting list;不帶此屬性者屬於元素樹 - 葉頁面帶
leaf屬性
運算子類別#
amprocnum | amproc
−−−−−−−−−−−+−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−
1 | gin_cmp_tslexeme
2 | pg_catalog.gin_extract_tsvector
3 | pg_catalog.gin_extract_tsquery
4 | pg_catalog.gin_tsquery_consistent
5 | gin_cmp_prefix
6 | gin_tsquery_triconsistent
(6 rows)| # | 函式 | 職責 |
|---|---|---|
| 1 | 比較函式 | 比較兩個元素(此例為兩個詞位)。若詞位由 B-tree 支援的一般型別表示,GIN 會自動沿用 B-tree 運算子類別定義的比較運算子 |
| 2 | extract_tsvector | 從文件中萃取詞位 |
| 3 | extract_tsquery | 從搜尋查詢中萃取詞位 |
| 4 | consistent | 一致性函式:取得「查詢指定的哪些詞位出現在文件中」的精確資訊 |
| 5 | cmp_prefix(選用) | 部分搜尋時檢查索引元素是否部分符合搜尋鍵(此例為依前綴搜尋詞位,如 c:*) |
| 6 | triconsistent | 一致性函式:在不確定的脈絡下運作,可在尚不清楚某些詞位是否存在於文件時被呼叫 |
文件與查詢用不同函式萃取是合理的:兩者至少由不同資料型別(
tsvector與tsquery)表示。此外,搜尋查詢的函式決定了搜尋將如何進行:若查詢要求文件必須含有特定詞位,搜尋就會被限縮到「至少含有查詢中一個詞位」的文件;若沒有這種條件(例如你要找不含某詞位的文件),就得掃描所有文件——當然昂貴得多。
運算子類別不必同時實作第 4 與第 6 個函式,只提供其中之一就夠——但搜尋效率可能因此受損。
tsvector_ops 只支援一個運算子,即比對文件與搜尋查詢的 @@(GiST 運算子類別中也有它)。
搜尋#
以 everywhere | oink 查詢為例,兩個詞位以 OR 連接:
- 支援函式從
tsquery型別的搜尋字串中萃取出詞位everywher與oink(搜尋鍵) - 由於查詢要求特定詞位存在,至少含有一個查詢鍵的文件,其 TID 被串成一份清單。做法是在詞位樹中搜尋每個搜尋鍵對應的 TID,並加入共同清單
- 索引中所有 TID 都是有序的,這讓數道排序過的 TID 串流得以合併成一道
- 每個找到的、對應到某文件的 TID,都由一致性函式檢查
到步驟 2 為止,鍵是以 AND、OR 還是其他運算子組合起來,都還無關緊要——搜尋引擎只面對一份鍵清單,對查詢語意一無所知。
是一致性函式在詮釋搜尋查詢,只留下滿足查詢(或至少可能滿足、須以資料表重新檢查)的 TID。

圖 28-3:`everywhere | oink` 查詢在詞位樹中走訪的路徑

圖 28-4:被走訪到的 posting tree 部分
此例中,一致性函式保留了所有 TID:
| TID | 「everywher」 | 「oink」 | 一致性函式 |
|---|---|---|---|
| (0,4) | ✓ | – | ✓ |
| (1,4) | ✓ | – | ✓ |
| (2,3) | – | ✓ | ✓ |
| (2,4) | ✓ | ✓ | ✓ |
搜尋查詢中也可以放前綴而非完整詞位。當應用程式使用者只輸入單字開頭幾個字母、卻期待立刻拿到結果時,這很有用。
例如
pig:*查詢會匹配所有含以pig開頭之詞位的文件——這裡會得到pigs;若 MacDonald 老先生的農場也養鴿子,pigeons同樣會中。
高頻與低頻詞位#
若被搜尋的詞位在文件中多次出現,建出的 TID 清單會很長,效率自然差。幸運的是,只要查詢中還含有一些低頻詞位,往往就能避開這個問題。
以 farm & cluck 查詢為例:cluck 出現兩次,farm 出現六次。演算法不會平等對待兩者並依它們建出完整的 TID 清單,而是:
- 把低頻的
cluck視為必要(mandatory) - 把高頻的
farm視為選用(optional)
因為就查詢語意而言,含 farm 的文件唯有同時含 cluck 才可能滿足查詢。
於是索引掃描先找出第一份含 cluck 的文件,其 TID 為 (1,3)。接著才去確認該文件是否也含 farm——但所有 TID 小於 (1,3) 的文件都可以跳過。
由於高頻詞位很可能對應到大量 TID,它們多半被存在獨立的樹中,因此連整批頁面都能跳過。此例中
farm詞位樹的搜尋直接從 (1,3) 開始。
這個最佳化也適用於超過兩個詞位的複雜情境:演算法依頻率排序詞位,逐一加入必要詞位清單,直到剩下的詞位不再足以保證文件滿足查詢為止。
延伸:三個詞位的判定過程——farm & ( cluck | chick )
最低頻的詞位是 chick,立刻被加入必要清單。為檢查其他詞位是否可視為選用,一致性函式對必要詞位取 false、對其餘詞位取 true:
- 得到
true AND (true OR false) = true,代表剩下的詞位「自給自足」,其中至少一個必須成為必要 - 加入次低頻的
cluck後,一致性函式回傳true AND (false OR false) = false
因此 chick 與 cluck 成為必要詞位,farm 維持選用。

圖 28-5:`farm & ( cluck | chick )` 查詢中,低頻詞位先行、高頻詞位可跳過的走訪路徑
posting list 長度為三(必要詞位共出現三次):
| TID | 「chick」 | 「cluck」 | 「farm」 | 一致性函式 |
|---|---|---|---|---|
| (1,2) | ✓ | – | ✓ | ✓ |
| (1,3) | – | ✓ | – | – |
| (1,4) | – | ✓ | – | – |
只要知道詞位頻率,就能以最有效率的方式合併詞位樹:從低頻詞位開始,跳過高頻詞位中確定冗餘的那些頁面範圍。這減少了一致性函式必須被呼叫的次數。
延伸:在 pgsql-hackers 郵件封存上驗證此最佳化
先挑一個常見詞與一個罕見詞:
=> SELECT word, ndoc
FROM ts_stat('SELECT tsv FROM mail_messages')
WHERE word IN ('wrote', 'tattoo');
word | ndoc
−−−−−−−−+−−−−−−−−
wrote | 231173
tattoo | 2
(2 rows)同時含有兩者的文件確實存在,且查詢幾乎和單獨搜尋 tattoo 一樣快:
=> SELECT count(*) FROM mail_messages
WHERE tsv @@ to_tsquery('wrote & tattoo');
count
−−−−−−−
1
(1 row)
Time: 0,631 ms
=> SELECT count(*) FROM mail_messages
WHERE tsv @@ to_tsquery('tattoo');
count
−−−−−−−
2
(1 row)
Time: 2,227 ms但若單獨搜尋 wrote,就慢上數百倍:
=> SELECT count(*) FROM mail_messages
WHERE tsv @@ to_tsquery('wrote');
count
−−−−−−−−
231173
(1 row)
Time: 343,556 ms插入#
GIN 索引不能含有重複值;若待加入的元素已存在於索引中,只要把它的 TID 加進既有元素的 posting list 或 posting tree 即可。
- posting list 是索引項目的一部分,在頁面中不能佔太多空間——超過配額時,清單會轉成一棵樹
- 新元素(或新 TID)加入樹時可能造成頁面溢位,此時頁面一分為二,元素在兩者之間重新分配
但每份文件通常含有許多待索引的詞位。因此即使你只建立或修改一份文件,索引樹仍會經歷大量修改——這正是 GIN 更新相當慢的原因。
以插入 TID 為 (4,1) 的「Everywhere clucks, moos, and oinks」一列為例:cluck、moo、oink 的 posting list 被延長,而 everywher 的清單超過最大尺寸,被切分成獨立的樹。

圖 28-6:插入新列後的索引狀態——`everywher` 的 posting list 已升格為獨立的 posting tree
fastupdate:延遲更新#
不過,若索引一次納入多份文件的變更,總工作量往往會比逐次變更來得少,因為這些文件可能共用一些詞位。這個最佳化由 fastupdate 儲存參數控制(預設啟用)。
延遲的索引更新累積在一份無序的 pending list 中,它實體存放於元素樹之外的獨立 list 頁面。當這份清單夠大時,其全部內容一次轉入索引,清單隨即清空。
清單的最大尺寸由
gin_pending_list_limit參數(預設 4MB)或同名的索引儲存參數定義。
延遲更新的代價:
- 拖慢搜尋——除了樹本身,還得掃描整份無序的詞位清單
- 插入時間變得較難預測——任何一次變更都可能引發溢位,進而觸發昂貴的合併程序
後者部分被緩解:合併也可以在索引 vacuum 期間非同步執行。
建立新索引時,元素同樣是批次加入而非逐一加入(後者太慢)。但此時所有變更不是存進磁碟上的無序清單,而是累積在一塊 maintenance_work_mem(預設 64MB)記憶體中,待該區塊無空間時再轉入索引。
就搜尋精確度而言,本章的範例證明了 GIN 優於 GiST 的簽章樹,因此全文檢索通常使用 GIN。
然而,GIN 更新緩慢的問題,可能會在資料被積極更新時讓天平倒向 GiST。
限制結果集大小#
原因正是那份無序的延遲更新清單:索引存取時會掃描該清單建出 bitmap,再以樹的資料更新這個 bitmap。若搜尋進行期間,該無序清單被合併進樹(因索引更新或 vacuum 所致),同一個值可能被回傳兩次——這無法接受。但對 bitmap 而言毫無問題:同一個位元不過被設定兩次而已。
因此,對 GIN 索引使用
LIMIT子句並不怎麼有效率——bitmap 仍得完整建出,這在總成本中佔了不小份額。
=> EXPLAIN SELECT * FROM mail_messages
WHERE tsv @@ to_tsquery('hacker')
LIMIT 1000;
QUERY PLAN
−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−
Limit (cost=481.41..1964.22 rows=1000 width=1258)
−> Bitmap Heap Scan on mail_messages
(cost=481.41..74939.28 rows=50214 width=1258)
Recheck Cond: (tsv @@ to_tsquery('hacker'::text))
−> Bitmap Index Scan on mail_gin_idx
(cost=0.00..468.85 rows=50214 width=0)
Index Cond: (tsv @@ to_tsquery('hacker'::text))
(7 rows)為此,GIN 提供了一項特殊功能來限制索引掃描回傳的結果數量:gin_fuzzy_search_limit 參數(預設為 0,即關閉)。啟用後,索引存取方法會隨機跳過部分值,以取得大致指定的列數(故名「模糊」):
=> SET gin_fuzzy_search_limit = 1000;
=> SELECT count(*)
FROM mail_messages
WHERE tsv @@ to_tsquery('hacker');
count
−−−−−−−
727
(1 row)
=> SELECT count(*)
FROM mail_messages
WHERE tsv @@ to_tsquery('hacker');
count
−−−−−−−
791
(1 row)注意這些查詢中沒有
LIMIT子句。這是「使用索引掃描與 heap 掃描卻得到不同資料」的唯一正當途徑。規劃器對 GIN 索引的這種行為一無所知,估算成本時不會納入此參數值。
屬性#
GIN 存取方法的所有屬性在各層級皆相同,不取決於特定的運算子類別。
存取方法層級:
amname | name | pg_indexam_has_property
−−−−−−−−+−−−−−−−−−−−−−−−+−−−−−−−−−−−−−−−−−−−−−−−−−
gin | can_order | f
gin | can_unique | f
gin | can_multi_col | t
gin | can_exclude | f
gin | can_include | f
(5 rows)- 不支援排序與唯一約束
- 支援多欄索引,但欄位順序無關緊要。與一般 B-tree 不同,多欄 GIN 索引不存放複合鍵,而是用對應的欄位編號去擴充個別元素
- 不支援排除約束,因為
index_scan屬性不可用 - 不支援
INCLUDE欄位——這裡意義不大,因為 GIN 索引幾乎不可能當作 covering index:它只含索引值的個別元素,值本身存在資料表中
索引層級:
name | pg_index_has_property
−−−−−−−−−−−−−−−+−−−−−−−−−−−−−−−−−−−−−−−
clusterable | f
index_scan | f
bitmap_scan | t
backward_scan | f
(4 rows)- 不支援逐一取得結果:索引存取永遠回傳 bitmap
- 基於同樣理由,依 GIN 索引重排資料表毫無意義:bitmap 永遠對應到資料在表中的實體佈局,不論那是什麼佈局
- 不支援反向掃描:該功能只對一般索引掃描有用,對 bitmap 掃描無用
欄位層級:所有欄位層級屬性都不可用——排序(理由顯然)、當作 covering index(文件本身未存於索引中)、以及 NULL 支援(對非原子型別的元素而言沒有意義)皆然。
GIN 的侷限與 RUM 索引#
GIN 雖然強大,仍無法應付全文檢索的所有挑戰:
tsvector型別雖然標示了詞位的位置,但這項資訊並未進入索引。因此 GIN 無法加速考慮詞位鄰近性的片語搜尋- 搜尋引擎通常依相關性回傳結果,而 GIN 不支援排序運算子,唯一的辦法是為每一列結果計算排名函式——當然非常慢
這些缺點由 RUM 存取方法解決(這名字讓人不禁懷疑開發者對 GIN 真義的說法是否誠懇)。它以擴充形式提供,可從 PGDG 套件庫下載,或直接取得原始碼。
RUM 以 GIN 為基礎,但有兩項主要差異:
- RUM 不提供延遲更新,因此除了 bitmap 掃描外也支援一般索引掃描,並實作了排序運算子
- RUM 的索引鍵可以帶額外資訊。這有點像
INCLUDE欄位,但這裡的額外資訊是繫結到特定鍵的。在全文檢索脈絡下,RUM 運算子類別把詞位出現處對映到它在文件中的位置,加速片語搜尋與結果排名
RUM 的代價:更新緩慢、索引體積更大。此外,由於 rum 是擴充,它仰賴通用 WAL 機制——比內建日誌慢,且產生更大量的 WAL。
三元組#
pg_trgm 擴充能藉由比較重合的三字母序列(三元組,trigram)數量來評估詞彙相似度。詞彙相似度可與全文檢索並用,即使搜尋詞打錯字也能回傳一些結果。
gin_trgm_ops 運算子類別實作了文字字串的索引。要切出文字值的元素,它萃取的是各種三字母子字串而非單字或詞位(只計字母與數字,其他字元一律忽略)。
索引中的三元組以整數表示。注意對非拉丁字元(在 UTF-8 編碼下佔二到四位元組)而言,這種表示法無法解碼回原本的符號。
=> CREATE EXTENSION pg_trgm;
=> SELECT unnest(show_trgm('macdonald')),
unnest(show_trgm('McDonald'));
unnest | unnest
−−−−−−−−+−−−−−−−−
m | m
ma | mc
acd | ald
ald | cdo
cdo | don
don | ld
ld | mcd
mac | nal
nal | ona
ona |
(10 rows)此類別支援字串與詞彙的精確與模糊比較運算子:
amopopr | oprcode
−−−−−−−−−−−−−−−−+−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−
%(text,text) | similarity_op
~~(text,text) | textlike -- LIKE
~~*(text,text) | texticlike -- ILIKE
~(text,text) | textregexeq -- 正規表達式
~*(text,text) | texticregexeq
%>(text,text) | word_similarity_commutator_op
%>>(text,text) | strict_word_similarity_commutator_op
=(text,text) | texteq
(8 rows)模糊比較時,字串間的距離可定義為「共同三元組數」與「查詢字串三元組總數」之比。
但如前所述,GIN 不支援排序運算子,因此類別中所有運算子都必須是布林的。
於是對實作模糊比較策略的
%、%>、%>>,一致性函式在算出的距離不超過既定門檻時回傳true。
對 = 與 LIKE 運算子,一致性函式要求該值含有查詢字串的所有三元組;比對正規表達式則需要複雜得多的檢查。
無論如何,三元組搜尋永遠是模糊的,結果必須重新檢查。
索引陣列#
陣列資料型別也受 GIN 支援。建在陣列元素上的 GIN 索引,可用來快速判定一個陣列是否與另一個陣列重疊或被包含:
amopopr | oprcode | amopstrategy
−−−−−−−−−−−−−−−−−−−−−−−+−−−−−−−−−−−−−−−−+−−−−−−−−−−−−−−
&&(anyarray,anyarray) | arrayoverlap | 1
@>(anyarray,anyarray) | arraycontains | 2
<@(anyarray,anyarray) | arraycontained | 3
=(anyarray,anyarray) | array_eq | 4
(4 rows)以 demo 資料庫的 routes 視圖為例,days_of_week 欄位是航班執飛日的陣列。要建索引,得先把視圖具體化:
=> CREATE TABLE routes_tbl AS
SELECT * FROM routes;
SELECT 710
=> CREATE INDEX ON routes_tbl USING gin(days_of_week);建出的索引只含七個元素:代表星期幾的整數 1 到 7。
查詢執行過程與前面全文檢索所示相當類似。此處搜尋查詢由一般陣列(而非特殊資料型別)表示,並假定被索引的陣列必須含有所有指定元素。
一項重要差異:相等條件還要求被索引的陣列不含其他元素。
一致性函式雖然從策略編號得知這項要求,卻無法驗證「沒有多餘元素」,因此它請求索引引擎以資料表重新檢查結果。
=> EXPLAIN (analyze, costs off, timing off, summary off)
SELECT * FROM routes_tbl
WHERE days_of_week = ARRAY[2,4,7];
QUERY PLAN
−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−
Bitmap Heap Scan on routes_tbl (actual rows=11 loops=1)
Recheck Cond: (days_of_week = '{2,4,7}'::integer[])
Rows Removed by Index Recheck: 482
Heap Blocks: exact=16
−> Bitmap Index Scan on routes_tbl_days_of_week_idx (actual ro...
Index Cond: (days_of_week = '{2,4,7}'::integer[])
(6 rows)btree_gin 擴充#
若想再依出發城市篩選,索引就缺了 departure_city 欄位——但一般純量資料型別並沒有實作 GIN 運算子類別:
=> CREATE INDEX ON routes_tbl 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-tree 處理的 GIN 運算子類別,做法是把純量值表示成只有單一元素的複合值。
=> CREATE EXTENSION btree_gin;
=> CREATE INDEX ON routes_tbl USING gin(days_of_week,departure_city);對
btree_gist的提醒在此同樣成立:B-tree 在比較操作上有效率得多,因此只有在真的需要 GIN 索引時才值得使用btree_gin。例如「小於」「小於等於」條件的搜尋,在 B-tree 中可用反向掃描完成,在 GIN 中則不行。
索引 JSON#
另一個內建 GIN 支援的非原子資料型別是 jsonb。它提供了一整套 JSON 運算子,其中有些可靠 GIN 加速。
有兩個運算子類別,各自從 JSON 文件萃取出不同的元素集合:jsonb_ops 與 jsonb_path_ops。
jsonb_ops 運算子類別#
jsonb_ops 是預設類別。原始 JSON 文件的所有鍵、值、陣列元素都被轉成索引項目。它可加速下列查詢:
amopopr | oprcode | amopstrategy
−−−−−−−−−−−−−−−−−−−−+−−−−−−−−−−−−−−−−−−−−−−−+−−−−−−−−−−−−−−
@>(jsonb,jsonb) | jsonb_contains | 7
?(jsonb,text) | jsonb_exists | 9
?|(jsonb,text[]) | jsonb_exists_any | 10
?&(jsonb,text[]) | jsonb_exists_all | 11
@?(jsonb,jsonpath) | jsonb_path_exists_opr | 15
@@(jsonb,jsonpath) | jsonb_path_match_opr | 16
(6 rows)- 包含(
@>) - 鍵是否存在(
?、?|、?&) - JSON path 匹配(
@?、@@)
以條件 route @> '{"days_of_week": [6]}'(挑出週六執飛的航班)為例:
- 支援函式從查詢的 JSON 值中萃取搜尋鍵:
days_of_week與6 - 在元素樹中搜尋這些鍵,至少含其中之一的文件交由一致性函式檢查
- 對「包含」策略,該函式要求所有搜尋鍵皆須具備
但結果仍必須以資料表重新檢查:從索引的角度來看,指定的路徑也可能對應到
{"days_of_week": [2], "foo": [6]}這樣的文件。

圖 28-7:`jsonb_ops` 建出的索引——所有鍵、值與陣列元素各自成為獨立的索引項目
jsonb_path_ops 運算子類別#
第二個類別 jsonb_path_ops 含有的運算子較少:
amopopr | oprcode | amopstrategy
−−−−−−−−−−−−−−−−−−−−+−−−−−−−−−−−−−−−−−−−−−−−+−−−−−−−−−−−−−−
@>(jsonb,jsonb) | jsonb_contains | 7
@?(jsonb,jsonpath) | jsonb_path_exists_opr | 15
@@(jsonb,jsonpath) | jsonb_path_match_opr | 16
(3 rows)使用此類別時,索引含的是從文件根部到所有值與所有陣列元素的「路徑」,而非孤立的 JSON 片段。這讓搜尋精確且有效率得多,但對「引數為個別鍵而非路徑」的操作沒有加速效果。
由於路徑可能相當長,實際被索引的不是路徑本身,而是它們的雜湊值。

圖 28-8:`jsonb_path_ops` 建出的索引——項目是「從根到值的完整路徑」的雜湊值
執行同樣的 route @> '{"days_of_week": [6]}' 條件時,支援函式萃取的是整條路徑「days_of_week, 6」而非它的個別組成,兩份匹配文件的 TID 因此立刻就能在元素樹中找到。
這些項目當然仍會經一致性函式檢查、再由索引引擎重新檢查(例如為了排除雜湊碰撞)。
但由於樹的搜尋有效率得多,只要
jsonb_path_ops的運算子所提供的索引支援足以應付查詢,就永遠該選它。
索引其他資料型別#
下列資料型別也有透過擴充提供的 GIN 支援:
| 型別 | 擴充 / 運算子類別 | 說明 |
|---|---|---|
| 整數陣列 | intarray / gin__int_ops | 與標準 array_ops 十分相似,但支援用來比對文件與搜尋查詢的 @@ 匹配運算子 |
| 鍵值儲存 | hstore / gin_hstore_ops | 實作鍵值對的儲存,鍵與值都會被索引 |
| JSON 查詢語言 | 外部 jsquery 擴充 | 提供自有的查詢語言與 JSON 的 GIN 索引支援 |
在 SQL:2016 標準被採納、且 PostgreSQL 實作了 SQL/JSON 查詢語言(PostgreSQL 12)之後,標準的內建能力看來是更好的選擇。