PostgreSQLGIN索引實現原理

德哥發表於2017-02-05

標籤

PostgreSQL , GIN , 核心 , 實現原理 , PostgreSQL資料庫核心分析


背景

本文參考並擴充套件自如下文件,修正了一些內容(大多數是由於版本不同造成的差異)

《PostgreSQL資料庫核心分析》 ( 成書較早,大量內容基於8.4的程式碼編寫 )

以及

http://zisedeqing.blog.163.com/blog/static/95550871201621623458216/ ( 大量內容參考自 PostgreSQL資料庫核心分析 )

術語

本文適用的一些術語

屬性 – 可能取自pg_attribute,指的是列。

postgres=# d pg_attribute  
              Table "pg_catalog.pg_attribute"  
    Column     |   Type    | Collation | Nullable | Default   
---------------+-----------+-----------+----------+---------  
 attrelid      | oid       |           | not null |   
 attname       | name      |           | not null |   
 atttypid      | oid       |           | not null |   
 attstattarget | integer   |           | not null |   
 attlen        | smallint  |           | not null |   
 attnum        | smallint  |           | not null |   
 attndims      | integer   |           | not null |   
 attcacheoff   | integer   |           | not null |   
 atttypmod     | integer   |           | not null |   
 attbyval      | boolean   |           | not null |   
 attstorage    | "char"    |           | not null |   
 attalign      | "char"    |           | not null |   
 attnotnull    | boolean   |           | not null |   
 atthasdef     | boolean   |           | not null |   
 attisdropped  | boolean   |           | not null |   
 attislocal    | boolean   |           | not null |   
 attinhcount   | integer   |           | not null |   
 attcollation  | oid       |           | not null |   
 attacl        | aclitem[] |           |          |   
 attoptions    | text[]    |           |          |   
 attfdwoptions | text[]    |           |          |   
Indexes:  
    "pg_attribute_relid_attnam_index" UNIQUE, btree (attrelid, attname)  
    "pg_attribute_relid_attnum_index" UNIQUE, btree (attrelid, attnum)  

元組 – tuple,指的是行(包括heap tuple, index tuple等,都可以叫元組)。

tid, ctid – 指行號(block_number, item pointer),包括HEAP表的BLOCK NUMBER,以及在BLOCK中指向行的item pointer。

鍵值 – 指索引結構中的KEY

基表 – 建立索引的表

1 概述

GIN(Generalized Inverted Index, 通用倒排索引) 是一個儲存對(key, posting list)集合的索引結構,其中key是一個鍵值,而posting list 是一組出現過key的位置。如(‘hello`, `14:2 23:4`)中,表示hello在14:2和23:4這兩個位置出現過,在PG中這些位置實際上就是元組的tid(行號,包括資料塊ID(32bit),以及item point(16 bit) )。

在表中的每一個屬性,在建立索引時,都可能會被解析為多個鍵值,所以同一個元組的tid可能會出現在多個key的posting list中。

通過這種索引結構可以快速的查詢到包含指定關鍵字的元組,因此GIN索引特別適用於多值型別的元素搜尋,比如支援全文搜尋,陣列中元素的搜尋,而PG的GIN索引模組最初也是為了支援全文搜尋而開發的。

說到這裡,你可能會覺得GIN的結構有點像b+tree,包括KEY和對應的值(posting list)。別急,請繼續往下看。

2 GIN索引的擴充套件開發

GIN是一個開放的索引介面,所以它不僅適用於已經存在的如陣列、全文檢索等資料型別,同樣也可以擴充套件支援更多的資料型別。

請使用者參考擴充套件指南如下,現在可能已經不止實現5種介面了。

https://www.postgresql.org/docs/9.6/static/gin-extensibility.html

GIN索引具有很好的可擴充套件性,允許在開發自定義資料型別時由該資料型別的領域專家(而非資料庫專家)設計適當的訪問方法,這些訪問方法只需考慮對於資料型別本身的語義處理,GIN索引自身可以處理併發控制、日誌記錄、搜尋樹結構等操作。

定義一個GIN訪問方法所要做的就是實現5個使用者定義的方法,這些方法定義了鍵值、鍵值與鍵值之間的關係、被索引值、能夠使用索引的查詢以及部分匹配。

這些方法是:

1. compare方法:比較兩個鍵值a和b,然後返回一個整數值,返回負值表示a < b,返回0表示a = b,返回正值表示a > b。

其函式原型如下:

int compare(Datum a, Datum b)  

2. extractValue方法:根據引數inputValue生成一個鍵值陣列,並返回其指標,鍵值陣列中元素的個數存放在另一個引數nkeys中。

其函式原型如下:

Datum *extractValue(Datum inputValue, int32 *nkeys)  

3. extractQuery方法:根據引數query生成一個用於查詢的鍵值陣列,並返回其指標。

函式原型如下:

Datum *extractQuery(Datum query, int32 *nkeys, StrategyNumber n, bool **pmatch, Pointer **extra_data)  

extractQuery通過引數n指定的操作符策略號來決定query的資料型別以及需要提取的鍵值,返回鍵值陣列的長度存放在nkeys引數中。

如果query中不包含鍵值,則nkeys可以為0或者-1:

nkeys = 0 表示索引中所有值都滿足查詢,將執行完全索引掃描(查詢null時是這樣); nkeys = -1 表示索引中沒有鍵值滿足查詢,跳過索引掃描。

在部分匹配時,輸出引數pmatch記錄返回的鍵值陣列中的每一個鍵值是否請求部分匹配。

輸出引數extra_data用來向consistent和comparPartial方法傳遞使用者自定義需要的資料。

4. consistent方法:用於檢查索引值是否滿足查詢,

其函式原型如下:

bool consistent(bool check[], StrategyNumber n, Datum query, int32 nkeys, Pointer extra_data[], bool *recheck)  

如果索引值滿足查詢則返回true,如果recheck = true,則返回true的索引還需要進一步的檢查。

recheck: 精確比較時recheck = false;否則recheck = true,通過索引找到的基表元組還需要進行是否滿足操作符的檢查(在TSVector型別時,如果key帶有權值,則recheck = true)。

PS :

如果索引KEY中區分了權值,則recheck依舊=false。 使用者需要看看各個版本的release notes或者跟蹤程式碼的迭代,比如PostgreSQL 9.6, postgrespro rum索引介面。

《PostgreSQL 全文檢索加速 快到沒有朋友 – RUM索引介面(潘多拉魔盒)》

《從難纏的模糊查詢聊開 – PostgreSQL獨門絕招之一 GIN , GiST , SP-GiST , RUM 索引原理與技術背景》

以查詢 fat & (cat | dog)為例,consistent的執行:

pic

PostgreSQL 9.4對GIN的掃描,儲存都進行了優化,例如可以跳過一些posting list的檢查或者一些ctid的檢查。(從最短的posting tree/list開始,跳過其他更小的ctid),請參考

《PostgreSQL GIN multi-key search 優化》

5. comparePartial方法:將部分匹配的查詢與索引值進行比較,返回值為負值表示兩者不匹配,但繼續索引掃描;返回值為0表示兩者匹配;返回值為正值表示停止掃描。

其函式原型如下:

int comparePartial(Datum partial_key, Datum key, StrategyNumber n, Pointer extra_data)  

第6種介面為可選介面,用於partial match,類似lossy index.

int comparePartial(Datum partial_key, Datum key, StrategyNumber n, Pointer extra_data)  

所以在PG中新增一種新的資料型別並且讓GIN支援該資料型別,則需要完成以下步驟:

1. 新增資料型別

2. 為新資料型別實現並註冊各種操作符所需要的函式,然後建立新型別的操作符

3. 用CREATE OPERATOR CLASS為新的資料型別建立一個操作符類,該語句需要指定GIN索引所需要的5個支援函式

PG的GIN索引,內部實現了對於TSVector資料型別的支援,並提供了把TEXT型別轉換成TSVector的介面,所以可以說PG的GIN索引支援TSVector和TEXT的資料型別。

3 GIN索引結構

邏輯結構

GIN索引在邏輯上可以看成一個relation,該relation有兩種結構:

1. 只索引基表的一列

key value
Key1 Posting list( or posting tree)
Key2 Posting list( or posting tree)

2. 索引基表的多列(複合、多列索引)

column_id key value
Column1 num Key1 Posting list( or posting tree)
Column2 num Key1 Posting list( or posting tree)
Column3 num Key1 Posting list( or posting tree)

這種結構,對於基表中不同列的相同的key,在GIN索引中也會當作不同的key來處理。

物理結構

GIN索引在物理儲存上包含如下內容:

1. Entry:GIN索引中的一個元素,可以認為是一個詞位,也可以理解為一個key

2. Entry tree:在Entry上構建的B樹

3. posting list:一個Entry出現的物理位置(heap ctid, 堆錶行號)的連結串列

4. posting tree:在一個Entry出現的物理位置連結串列(heap ctid, 堆錶行號)上構建的B樹,所以posting tree的KEY是ctid,而entry tree的KEY是被索引的列的值

5. pending list:索引元組的臨時儲存連結串列,用於fastupdate模式的插入操作

從上面可以看出GIN索引主要由Entry tree和posting tree(or posting list)組成,其中Entry tree是GIN索引的主結構樹,posting tree是輔助樹。

entry tree類似於b+tree,而posting tree則類似於b-tree。

另外,不管entry tree還是posting tree,它們都是按KEY有序組織的。

1 Entry tree

entry tree是一個B樹,與Btree索引類似,用來組織和儲存(key, posting list)對,其樹結構如下:

pic

從上圖可以看出非葉子節點的每個元組都有一個指向孩子節點的指標(child pointer),該指標由索引元組結構的tid(表示下層資料塊ID,即下層GIN索引資料塊ID)來表示,中間節點和葉子節點還有一個右兄弟節點指標,指向其右兄弟節點,該指標記錄在GinPageOpaqueData的rightlink內(即索引頁的special部分,在頁面的尾部)。

src/include/access/gin_private.h

/*  
 * Page opaque data in an inverted index page.  
 *  
 * Note: GIN does not include a page ID word as do the other index types.  
 * This is OK because the opaque data is only 8 bytes and so can be reliably  
 * distinguished by size.  Revisit this if the size ever increases.  
 * Further note: as of 9.2, SP-GiST also uses 8-byte special space, as does  
 * BRIN as of 9.5.  This is still OK, as long as GIN isn`t using all of the  
 * high-order bits in its flags word, because that way the flags word cannot  
 * match the page IDs used by SP-GiST and BRIN.  
 */  
typedef struct GinPageOpaqueData  
{  
        BlockNumber rightlink;          /* next page if any */  
        OffsetNumber maxoff;            /* number of PostingItems on GIN_DATA &  
                                                                 * ~GIN_LEAF page. On GIN_LIST page, number of  
                                                                 * heap tuples. */  
        uint16          flags;                  /* see bit definitions below */  
} GinPageOpaqueData;  

typedef GinPageOpaqueData *GinPageOpaque;  

#define GIN_DATA                  (1 << 0)  
#define GIN_LEAF                  (1 << 1)  
#define GIN_DELETED               (1 << 2)  
#define GIN_META                  (1 << 3)  
#define GIN_LIST                  (1 << 4)  
#define GIN_LIST_FULLROW  (1 << 5)              /* makes sense only on GIN_LIST page */  
#define GIN_INCOMPLETE_SPLIT (1 << 6)   /* page was split, but parent not  
                                                                                 * updated */  
#define GIN_COMPRESSED    (1 << 7)  

/* Page numbers of fixed-location pages */  
#define GIN_METAPAGE_BLKNO      (0)  // meta資料塊固定為0號
#define GIN_ROOT_BLKNO          (1)  // root資料塊固定為1號

entry tree的非葉子節點與普通的btree樹的非葉子節點類似。

其葉子節點與普通btree的葉子節點不同,普通btree的葉子節點指向其索引的元組,而entry tree的葉子節點指向posting list,或者是posting tree。該指標用索引元組結構的tid表示。

具體如何區分posting list和posting tree將在頁面結構中介紹。

從上圖可以看出,如果posting list退化成單個item pointer,則GIN索引的結構就與B樹索引完全一樣(其實也不完全一樣,如下)。

PS :

gin 的btree和PostgreSQL的nbtree不一樣,相比nbtree更簡化,(比如nbtree的同級PAGE包含了雙向連結,而gin btree只有right link)。

參考

《深入淺出PostgreSQL B-Tree索引結構》

《B-Tree和B+Tree》

2 posting tree

posting tree與entry tree 類似,也是一個B樹,其樹結構與entry tree完全一樣,不同之處就是posting tree頁面儲存的元組內容與entry tree不同,如下:

pic

posting tree 非葉子節點,KEY是堆錶行號,VALUE是下層節點的塊ID。

posting tree 葉子節點,是堆錶行號list, 即posting list,(PostgreSQL使用了segment進行管理,將posting list中儲存的item point(堆錶行號)有序分段,壓縮儲存)。

暫時無法理解的話,可以繼續往下看,有細節介紹。

3 pending list

pending list是在fastupdate時,用來臨時快取GIN索引元組的,該連結串列把索引的插入操作推遲到一定條件時,批量處理。其結構是一個單向連結串列,如下:

pic

從上圖可以看出,pending list的meta頁面用來指示pending list的頭和尾的頁面號,沒有pending list的資料頁面,存放的是新的索引元組。

Index entries that appear in "pending list" pages work a tad differently as
well.  The optional column number, key datum, and null category byte are as
for other GIN index entries.  However, there is always exactly one heap
itempointer associated with a pending entry, and it is stored in the t_tid
header field just as in non-GIN indexes.  There is no posting list.
Furthermore, the code that searches the pending list assumes that all
entries for a given heap tuple appear consecutively in the pending list and
are sorted by the column-number-plus-key-datum.  The GIN_LIST_FULLROW page
flag bit tells whether entries for a given heap tuple are spread across
multiple pending-list pages.  If GIN_LIST_FULLROW is set, the page contains
all the entries for one or more heap tuples.  If GIN_LIST_FULLROW is clear,
the page contains entries for only one heap tuple, *and* they are not all
the entries for that tuple.  (Thus, a heap tuple whose entries do not all
fit on one pending-list page must have those pages to itself, even if this
results in wasting much of the space on the preceding page and the last
page for the tuple.)

4 GIN索引的頁面和元組結構

頁面結構

GIN索引共有6種型別的頁面:

型別 說明
GIN_DATA (1 << 0) 存放posting tree的頁面
GIN_LEAF (1 << 1) 葉子頁面
GIN_DELETED (1 << 2) 被標誌刪除的頁面
GIN_META (1 << 3) Gin索引的元頁面
GIN_LIST (1 << 4) Pending list頁面
GIN_LIST_FULLROW (1 << 5) 被填滿的GIN_LIST頁面

PS :

實際上現在的版本已經有8種頁面型別

src/include/access/gin_private.h

#define GIN_DATA                  (1 << 0)  
#define GIN_LEAF                  (1 << 1)  
#define GIN_DELETED               (1 << 2)  
#define GIN_META                  (1 << 3)  
#define GIN_LIST                  (1 << 4)  
#define GIN_LIST_FULLROW  (1 << 5)              /* makes sense only on GIN_LIST page */  
#define GIN_INCOMPLETE_SPLIT (1 << 6)   /* page was split, but parent not  
                                                                                 * updated */  
#define GIN_COMPRESSED    (1 << 7)  

1 meta頁面

GIN索引的元資訊頁面,其頁面保留區的flags有GIN_META標記。GIN索引的元資訊頁面的blocknum為0,meta頁面的結構如下:

typedef struct GinMetaPageData  
{  
        /*  
         * Pointers to head and tail of pending list, which consists of GIN_LIST  
         * pages.  These store fast-inserted entries that haven`t yet been moved  
         * into the regular GIN structure.  
         */  
        BlockNumber head;  
        BlockNumber tail;  
    // 在fast-inserted時使用,用來儲存由GIN_LIST頁面組成的pending list的頭和尾的頁面號。這些pending list是還沒有插入到GIN索引樹上的頁面。  

        /*  
         * Free space in bytes in the pending list`s tail page.  
         */  
        uint32          tailFreeSize;  
    // pending list尾部頁面的空閒空間  

        /*  
         * We store both number of pages and number of heap tuples that are in the  
         * pending list.  
         */  
        BlockNumber nPendingPages;  
    // pending list的頁面個數  

        int64           nPendingHeapTuples;  
    // pending list的heap 元組個數  

        /*  
         * Statistics for planner use (accurate as of last VACUUM)  
         */  
        BlockNumber nTotalPages;  
        BlockNumber nEntryPages;  
        BlockNumber nDataPages;  
        int64           nEntries;  
    // 靜態統計資訊,由最近的一次VACUUM計算得出  

        /*  
         * GIN version number (ideally this should have been at the front, but too  
         * late now.  Don`t move it!)  
         *  
         * Currently 2 (for indexes initialized in 9.4 or later)  
         *  
         * Version 1 (indexes initialized in version 9.1, 9.2 or 9.3), is  
         * compatible, but may contain uncompressed posting tree (leaf) pages and  
         * posting lists. They will be converted to compressed format when  
         * modified.  
         *  
         * Version 0 (indexes initialized in 9.0 or before) is compatible but may  
         * be missing null entries, including both null keys and placeholders.  
         * Reject full-index-scan attempts on such indexes.  
         */  
        int32           ginVersion;  
    // GIN索引的版本號  
} GinMetaPageData;  

目前GIN索引的後設資料頁面主要記錄pending list的相關資訊、統計資訊和版本號。

2 GIN索引page的 special區

GIN索引頁面的special區,用來儲存GIN索引相關的資訊,與BTree的BTPageOpaqueData類似,主要是建立樹形結構。

GIN索引頁面的special區結構如下:

/*  
 * Page opaque data in an inverted index page.  
 *  
 * Note: GIN does not include a page ID word as do the other index types.  
 * This is OK because the opaque data is only 8 bytes and so can be reliably  
 * distinguished by size.  Revisit this if the size ever increases.  
 * Further note: as of 9.2, SP-GiST also uses 8-byte special space, as does  
 * BRIN as of 9.5.  This is still OK, as long as GIN isn`t using all of the  
 * high-order bits in its flags word, because that way the flags word cannot  
 * match the page IDs used by SP-GiST and BRIN.  
 */  
typedef struct GinPageOpaqueData  
{  
        BlockNumber rightlink;          /* next page if any */  
        // 右兄弟節點的頁面號,用來建立兄弟連結串列,把所有處於同一層次的節點連線成一個單向連結串列  

        OffsetNumber maxoff;            /* number of PostingItems on GIN_DATA & ~GIN_LEAF page.   // 記錄PostingItems的長度  
                     * On GIN_LIST page, number of heap tuples. */  // 記錄posting list的長度  

        uint16          flags;                  /* see bit definitions below */  
    // 頁面標記,用來標記頁面的型別  
} GinPageOpaqueData;  

頁面型別flag定義如下  
#define GIN_DATA                  (1 << 0)  
#define GIN_LEAF                  (1 << 1)  
#define GIN_DELETED               (1 << 2)  
#define GIN_META                  (1 << 3)  
#define GIN_LIST                  (1 << 4)  
#define GIN_LIST_FULLROW  (1 << 5)              /* makes sense only on GIN_LIST page */  
#define GIN_INCOMPLETE_SPLIT (1 << 6)   /* page was split, but parent not  
                                                                                 * updated */  
#define GIN_COMPRESSED    (1 << 7)  

3 entry tree頁面

src/backend/access/gin/ginentrypage.c

entry tree是GIN索引的主樹,用來組織和儲存entry。

1. entry tree 非葉子頁面

非葉子頁面不帶任何標記資訊,entry tree的非葉子頁面結構與普通btree的非葉子頁面結構基本上是一樣的(但實際上與PostgreSQL nbtree有差異,如, 並非雙向連結串列),如下:

pic

2. entry tree 葉子頁面

葉子頁面帶有GIN_LEAF標記,表示是entry tree的葉子頁面。entry tree的葉子頁面與普通btree葉子頁面結構類似,只是在元組結構上有所不同,(實際上與PostgreSQL nbtree有差異,如, 並非雙向連結串列),如下:

pic

具體不同的地方參見元組結構的介紹

PS :

構建indextuple,注意單列索引和多列索引的區別

        /* Build the basic tuple: optional column number, plus key datum */
        if (ginstate->oneCol)
        {
                datums[0] = key;
                isnull[0] = (category != GIN_CAT_NORM_KEY);
        }
        else
        {
                datums[0] = UInt16GetDatum(attnum);
                isnull[0] = false;
                datums[1] = key;
                isnull[1] = (category != GIN_CAT_NORM_KEY);
        }

4 posting tree頁面

src/backend/access/gin/gindatapage.c

posting tree 是gin的輔助樹,用來組織超長的posting list,以加快其查詢速度。該樹的頁面是由GIN_DATA標記。

/*  
 * Data (posting tree) pages  
 *  
 * Posting tree pages don`t store regular tuples. Non-leaf pages contain  
 * PostingItems, which are pairs of ItemPointers and child block numbers.  
 * Leaf pages contain GinPostingLists and an uncompressed array of item  
 * pointers.  
 *  
 * In a leaf page, the compressed posting lists are stored after the regular  
 * page header, one after each other. Although we don`t store regular tuples,  
 * pd_lower is used to indicate the end of the posting lists. After that, free  
 * space follows.  This layout is compatible with the "standard" heap and  
 * index page layout described in bufpage.h, so that we can e.g set buffer_std  
 * when writing WAL records.  
 *  
 * In the special space is the GinPageOpaque struct.  
 */  

1. posting tree 非葉子頁面

非葉子頁面只有GIN_DATA標記,其頁面結構如下:

pic

PageHeader後面緊跟的一個ItemPointer是指該PAGE的right bound,即它所指引的所有下級節點中,最大指向的HEAP tid。

比如你抓了一手撲克牌,按牌面從小到大順序分成了若干堆牌,你有指向每一堆牌的方法(PostingItem),同時也知道最大的牌是什麼(PageHeader後面緊跟的一個ItemPointer)

src/backend/access/gin/README

請仔細閱讀

Posting tree  
------------  

If a posting list is too large to store in-line in a key entry, a posting tree  
is created. A posting tree is a B-tree structure, where the ItemPointer is  
used as the key.  

Internal posting tree pages use the standard PageHeader and the same "opaque"  
struct as other GIN page, but do not contain regular index tuples. Instead,  
the contents of the page is an array of PostingItem structs. Each PostingItem  
consists of the block number of the child page, and the right bound of that  
child page, as an ItemPointer. The right bound of the page is stored right  
after the page header, before the PostingItem array.  

與普通btree的頁面類似,不同是其儲存的元組是PostingItem,PostingItem格式為:

/*  
 * Posting item in a non-leaf posting-tree page  
 */  
typedef struct  
{  
        /* We use BlockIdData not BlockNumber to avoid padding space wastage */  
        BlockIdData child_blkno;  
    // 是孩子節點的頁面號,用於建立posting tree的層次關係。使用BlockIdData型別而不是BlockNumber的原因是避免空間浪費。  

        ItemPointerData key;  
    // 是posting list中的key,實際上就是該孩子節點的最小?heap tid  
} PostingItem;  

/*  
 * ItemPointer:  // 即tid or ctid  
 *  
 * This is a pointer to an item within a disk page of a known file  
 * (for example, a cross-link from an index to its parent table).  
 * blkid tells us which block, posid tells us which entry in the linp  
 * (ItemIdData) array we want.  
 *  
 * Note: because there is an item pointer in each tuple header and index  
 * tuple header on disk, it`s very important not to waste space with  
 * structure padding bytes.  The struct is designed to be six bytes long  
 * (it contains three int16 fields) but a few compilers will pad it to  
 * eight bytes unless coerced.  We apply appropriate persuasion where  
 * possible, and to cope with unpersuadable compilers, we try to use  
 * "SizeOfIptrData" rather than "sizeof(ItemPointerData)" when computing  
 * on-disk sizes.  
 */  
typedef struct ItemPointerData  
{  
        BlockIdData ip_blkid;  
        OffsetNumber ip_posid;  
}  

2. posting tree 葉子頁面

葉子頁面的標記為GIN_DATA|GIN_LEAF,其頁面結構如下:

pic

與正常的索引頁面類似,開始是頁面頭資訊,結尾是special區,不同的是中間區用來記錄posting list(即HEAP CTID)。

注意posting list會分段壓縮儲存,包括用於SKIP優化等。

src/backend/access/gin/README

請仔細閱讀

Posting tree leaf pages also use the standard PageHeader and opaque struct,  
and the right bound of the page is stored right after the page header, but  
the page content comprises of a number of compressed posting lists. The  
compressed posting lists are stored one after each other, between page header  
and pd_lower. The space between pd_lower and pd_upper is unused, which allows  
full-page images of posting tree leaf pages to skip the unused space in middle  
(buffer_std = true in XLogRecData).  

The item pointers are stored in a number of independent compressed posting  
lists (also called segments), instead of one big one, to make random access  
to a given item pointer faster: to find an item in a compressed list, you  
have to read the list from the beginning, but when the items are split into  
multiple lists, you can first skip over to the list containing the item you`re  
looking for, and read only that segment. Also, an update only needs to  
re-encode the affected segment.  

5 pending list 頁面

與entry tree的頁面類似,如下:

pic

不同之處是元組的結構,將在元組結構中介紹。

special區有一個指標,用來指向頁面的下一個頁面,這樣就把所有的pending list頁面以單連結串列的方式組織起來。

元組結構

1 entry tree 內的 indextuple 元組

entry tree的元組依然使用IndexTuple來表示,其結構為:

src/include/access/itup.h

/*  
 * Index tuple header structure  
 *  
 * All index tuples start with IndexTupleData.  If the HasNulls bit is set,  
 * this is followed by an IndexAttributeBitMapData.  The index attribute  
 * values follow, beginning at a MAXALIGN boundary.  
 *  
 * Note that the space allocated for the bitmap does not vary with the number  
 * of attributes; that is because we don`t have room to store the number of  
 * attributes in the header.  Given the MAXALIGN constraint there`s no space  
 * savings to be had anyway, for usual values of INDEX_MAX_KEYS.  
 */  

typedef struct IndexTupleData  
{  
        ItemPointerData t_tid;          /* reference TID to heap tuple */  

        /* ---------------  
         * t_info is laid out in the following fashion:  
         *  
         * 15th (high) bit: has nulls  
         * 14th bit: has var-width attributes  
         * 13th bit: unused  
         * 12-0 bit: size of tuple  
         * ---------------  
         */  

        unsigned short t_info;          /* various info about tuple */  
} IndexTupleData;   

但是對於不同的節點,其t_tid和後面的key會有所不同。

1. 非葉子節點

pic

與普通索引的元組結構一樣,由IndexTupleData + key組成,KEY儲存的都是被索引列的值,不同的是,其t_tid不是指向heap 元組,而是指向孩子頁面。

2. 葉子節點

葉子節點的元組是由IndexTupleData + key 或者是IndexTupleData + key + posting list表示的,對於posting list超長的情況,元組只記錄posting list對於的posting tree的root 節點頁面號,所以其元組結構如下:

pic

注意entry tree 葉子節點的tid已經沒有指向意義(指向KEY對應的heap的ctid)了,因為具有指向意義的內容儲存在VALUE裡面: 即posting list, 或者指向posting tree root page的pointer。

那麼entry tree 葉子節點的tid用來幹什麼呢?如下。

元組結構a(posting list) :

src/backend/access/gin/ginpostinglist.c

該結構的元組,由於posting list太長,無法儲存在元組內部,所以把posting list採用外部儲存,索引元組只記錄posting tree的root頁面號。

為了區分這兩種結構,使用元組中的tid中的ip_posid(tid ItemPointerData結構的後半部,只有2位元組)來區分,

ip_posid == GIN_TREE_POSTING,則表示記錄的是posting tree,此時tid的ip_blkid用來儲存posting tree的root頁面號。

元組結構b(posting tree) :

src/backend/access/gin/gindatapage.c

該結構的元組,把posting list直接儲存到key後面的連續空間中,使用tid的ip_posid(2位元組)來儲存posting list的長度,tid的ip_blkid(4位元組)來儲存posting list在元組內部的偏移?。

注:

GIN索引頁面至少要儲存3個索引元組(實際上有改進空間,nbtree雙向連結串列才有這個限制),所以對於8K的資料塊,indextuple(索引元組)的最大值大約是8192/3 = 2730, 每個itempointer為48bit,所以一個索引元組最多可以儲存(2730*8)/48 = 455個ctid,(如果是32K的資料塊呢,算一算,資料塊大小編譯PostgreSQL軟體時使用configure指定), GIN_TREE_POSTING定義為0xffff(保證與ip_posid 型別長度一致 2 bytes)。

2 posting tree 內的 indextuple 元組

posting tree的元組格式比較簡單,就是itempointer或者postingitem:

非葉子節點:

[child pointer (指向孩子節點)] [item pointer (孩子節點的最小?heap ctid)]  

葉子節點:

[item pointer list]  (posting list分段壓縮儲存)

實際上posting tree的葉子節點物理結構沒有這麼簡單,item pointer list在葉子節點中可能分段,壓縮,以posting list形式儲存,見src/backend/access/gin/README。

3 pending list 內的 indextuple 元組

pending list的頁面儲存的是臨時的索引元組,其元組格式為:

[tid] [flags] [key]  

其中tid指向的是heap元組,這與普通元組一樣。

key 為被索引的列值(row tuple)

5 GIN索引的構建

GIN索引的構建是根據基表構建GIN索引,在PG中通過index_build介面呼叫索引註冊的build函式完成GIN索引的構建。index_build是一個通用介面,該介面會根據索引的不同型別,自動呼叫合適的build函式,GIN索引的build介面是ginbuild介面。

GIN索引在構建時,會呼叫使用者定義的compare介面和extractValue介面,compare介面用來實現entry的比較,而extractValue介面用來把基表的屬性值提取出對應的entry。

GIN索引在構建時為了提高效能,使用了一種RB二叉樹的結構來快取索引元組,然後在RB二叉樹大於maintenance_work_mem時,批量的把RB樹中的索引元組插入到GIN的entry tree中。

GIN索引的構建流程是:

1. 初始化GinState結構

主要是從系統表中讀取GIN索引支援的那5個使用者自定義函式:compare、extractValue、extractQuery、consistent、comparePartial

2. 初始化meta和root頁面

其中meta頁面的blkno是0,root頁面的blkno是1

3. 記錄構建日誌

4. 初始化構建時的臨時記憶體上下文和用於快取的RB樹

5. 呼叫IndexBuildHeapScan掃描基表,並呼叫ginBuildCallback對每個基表的索引屬性處理

ginBuildCallback實現對每個基表列的處理:

a) 對每一個索引列,呼叫extractValue介面提取entry值

b) 把所有的對插入到RB樹中

c) 如果RB樹大於maintenance_work_mem,則把RB樹中的對插入到GIN索引中

此處在查詢entry的插入位置時,會呼叫compare介面比較兩個entry之間的大小

6. 把RB樹中的所有索引元組插入到GIN的entry tree中

7. 結束

6 GIN索引的掃描

GIN索引的掃描是根據掃描條件,同GIN索引中查詢滿足條件的基表元組,GIN索引的掃描介面與btree索引類似:ginbeginscan/ ginrescan/ ginendscan/ gingetbitmap,不同之處是GIN索引沒有提供返回單條基表元組的介面(即類似於btgettuple的介面)。

GIN索引掃描的基本用法是:

gscan = ginbeginscan(heap, nkeys);  

ginrescan(gscan, scankey);  

ntids = gingetbitmap(gscan, &btmap);  

while(BitmapHeapNext(btmap))  

{  

         // do something;  

}  

ginendscan(gscan)  

從上面可以看出GIN索引的掃描結果是一個bitmap,裡面儲存的是所有滿足條件的基表元組的tid。

ScanKey TO GinScanKey (where 轉換)

scankey描述了SQL語句的where的條件,pg中使用ScanKeyData來描述,每一個ScanKeyData描述一個條件,ScanKeyData[]的陣列描述了所有ScanKeyData的AND操作。而每一個ScanKeyData[]陣列對應於一次掃描,所以對於有OR的查詢,在執行時至少分成兩個掃描,輸出結果是兩個掃描結果集的並集。對於如下的where條件A and B or C,分成兩個掃描A and B 和C。我們研究的重點在於對索引的一次掃描。

對應於全文檢索,如下的查詢:

r1 @@ to_tsquery(`A | B`) and r2 @@ to_tsquery(`C & D`) or r3 @@ to_tsquery(`E| F`)  

其會分成:

scan1: r1 @@ to_tsquery(`A | B`) and r2 @@ to_tsquery(`C & D`)  

scan2: r3 @@ to_tsquery(`E| F`)  

結果是:scan1 U(並集) scan2

以一次掃描為例,在GIN掃描時,系統會先把scankey轉換成GinScanKey,然後根據GinScanKey進行索引的過濾。一個scankey會轉換成一個GinScanKey,而每個GinScanKey又會包含多個GinScanEntry,每個GinScanEntry表示scankey中的to_tsquery中的每一項。以r1 @@ to_tsquery(`A | B`) and r2 @@ to_tsquery(`C & D`)為例,其scankey為:

ScanKey[0] : r1 @@ to_tsquery(`A | B`)  

ScanKey[1] : r2 @@ to_tsquery(`C & D`)  

其轉換後的結構是:

pic

轉換的實現是通過使用者定義函式extractQuery來完成的,還以上面的查詢為例,系統對每一to_tsquery(`A | B`)型別的查詢呼叫extractQuery,提取出每個用於查詢的鍵值(對於to_tsquery(`A | B`)提取後的鍵值是querykey = {“A”, “B”}),然後對每個查詢鍵值建立一個GinScanEntry。GIN索引的每個GinScanEntry就是對GIN索引的一次掃描。

如下:

pic

gingetbitmap GIN掃描介面

gingetbitmap是實現GIN掃描的介面,該介面根據GinScanKey把滿足過濾條件的所有基表元組的tid儲存到bitmap中。

bitmap的大小由work_mem引數控制,如果gin索引掃描出過多元組,則bitmap會自動的根據需要選擇lossy儲存。bitmap的lossy儲存是不再儲存元組的tid而是直接儲存元組所在頁面的blkno。由於此種儲存bitmap沒有儲存具體元組,所以在執行層必須對bitmap返回的元組做recheck。

對於GIN索引,除了上面的情況下gin返回的元組需要做recheck外,還有一種情況需要做recheck:consistent方法會根據查詢設定是否需要做recheck。

我們還以查詢r1 @@ to_tsquery(`A | B`) and r2 @@ to_tsquery(`C & D`)來說明gingetbitmap實現原理。查詢r1 @@ to_tsquery(`A | B`) and r2 @@ to_tsquery(`C & D`),會分解為2個GinScanKey:GinScanKey1(r1 @@ to_tsquery(`A | B`))和GinScanKey2(r2 @@ to_tsquery(`C & D`)),這兩個條件的關係是∩,而GinScanKey1又分解為2個entry scan:entryA ∪entryB;GinScanKey2分解為entryC ∩ entryD。每個entry scan掃描的結果都是一個posting list(posting tree也是posting list),因此r1 @@ to_tsquery(`A | B`) and r2 @@ to_tsquery(`C & D`)就轉化為:

(plA ∪ plB) ∩ (plC ∩ plD), 其中pl是posting list的縮寫

即對posting list集合的邏輯運算,運算的結果構成的集合就是查詢的結果。

gingetbitmap會呼叫使用者4個自定義的介面:compare、extractQuery、consistent、comparePartial。compare在entry scan時用於比較兩個entry key的大小;extractQuery用來把查詢字串轉換成entry key;consistent用來合併每一個GinScanKey的結果集;comparePartial用來實現部分匹配。gingetbitmap的流程如下:

1. 把ScanKey轉換成GinScanKey

會呼叫extractQuery把查詢字串轉換成entry key,然後對每個entry key建立一個GinEntryScan

2. 掃描pending list,把滿足條件的基表元組tid加入到bitmap中

3. 對每個GinEntryScan進行掃描,找到GinEntryScan的key對應的葉子節點

a) 如果是部分匹配,則把所有滿足部分匹配的基表元組儲存GinEntryScan的臨時bitmap中

會呼叫comparePartial進行索引entry 與查詢key之間的部分匹配

b) 如果是精確查詢,則把索引元組的posting list或者posting tree的root頁面的posting list,儲存到GinEntryScan的list中

4. 迴圈獲取滿足查詢條件的基表元組:

a) 對每一個GinScanKey,呼叫consistent,合併GinScanKey中所有GinEntryScan的結果

b) 把所有GinScanKey的結果合併,一次一條的返回

c) 把滿足條件的基表元組tid插入到bitmap中

5. 返回查詢到的基表元組個數

7 GIN索引的insert和fastupdate優化

GIN索引的插入操作與btree索引不同,對於btree索引,基表增加一行,btree索引也是增加一個索引項。而對於GIN索引基表增加一行,GIN索引可能需要增加多個索引項。所以GIN索引的插入是低效的。所以PG為了解決這個問題,實現了兩種插入模式:

1. 正常模式

在該模式下,基表元組產生的新的GIN索引,會被立即插入到GIN索引

2. fastupdate模式

在該模式下,基表元組產生的新的GIN索引,會被插入到pending list中,而pending list會在一定條件下批量的插入到GIN索引中

下面就說明一下fastupdate模式的插入。

2.1 開啟和關閉fastupdate模式

可以通過create index 的WITH FASTUPDATE = OFF來關閉fastupdate模式,預設情況下是開啟fastupdate模式

2.2 對索引掃描的影響

在fastupdate模式下,新的索引元組以追加的方式插入到pending list中,不會進行任何的排序和去重操作,所以,在掃描時,只能順序掃描,因此pending list的掃描效率是非常低的,必須保證pending list的大小不要太大

2.3 對插入的影響

通常情況下,在fastupdate模式下,基表的更新效率是比較高的,但是如果一個事務的更新剛好讓pending list到達臨界點,而導致合併操作,則會使該事務比正常的事務慢很多

2.4 pending list的合併

把pending list的索引元組合併到GIN索引樹上有2種觸發條件:

1) 當pending list所佔空間大於work_mem時

PS

(有gin_pending_list_limit引數的版本,通過gin_pending_list_limit引數來控制,而非work_mem)

2) 在vacuum 索引的基表時(包括autovacuum在內)

因此可以根據autovacuum的間隔時間和work_mem來控制pending list的大小,避免其過大而拖慢掃描速度

在pending list合併時,其採用與GIN索引構建時相同的方式,即先把pending list內的資料,組織成一個RB樹,然後再把RB樹合併到GIN索引上。RB樹可以把pending list中無序的資料變成有序,並且可以合併重複key的項,提高插入效率。

8 GIN索引的vacuum

GIN索引的vacuum是用來清理無用的posting list或者posting tree的,GIN索引的vacuum與btree索引的vacuum一樣,提供了兩個介面ginbulkdelete和ginvacuumcleanup。

GIN索引的vacuum主要是清理entry tree和posting tree,如果entry的posting list為空了,vacuum依然不會刪除該entry,說明entry tree中的entry永遠不會被刪除;對於posting tree,如果posting tree也空了,在系統依然會把posting tree的root頁面保留,並關聯到entry上面。

9 GIN索引的併發控制

參考 《PostgreSQL資料庫核心分析》

10 GIN索引的日誌

參考 《PostgreSQL資料庫核心分析》

11 TSVector型別的GIN索引

PG預設提供了對TSVector資料型別的GIN索引的支援,並提供了對TEXT型別轉換TSVector型別的介面,因此PG在對TEXT型別的屬性建立GIN索引時,需要使用to_tsvector介面把TEXT型別轉換成TSVector。

TSVector

TSVector是PG中的一種資料型別,用來實現全文搜尋。它實際上是一個的陣列,其中key是一個關鍵詞,pos list是key在字串中出現的位置列表。如字串:

`Before you can use PostgreSQL you need to install it`  

在轉換成TSVector後的值為:

[Before, 1] [you, 2:6] [can, 3] [use, 4] [PostgreSQL, 5] [need, 7] [to, 8] [install, 9] [it, 10]  

因此TSVector實際上就是一組key和其出現位置的集合。

在程式碼中使用如下結構表示TSVector:

typedef struct  

{  

         int32                  vl_len_;  

         int32                  size;  

         WordEntry        entries[1];                  /* var size */  

         /* lexemes follow */  

} TSVectorData;  

其中WordEntry為:

typedef struct  

{  

         uint32  

                                     haspos:1,  

                                     len:11,                        /* MAX 2Kb */  

                                     pos:20;                        /* MAX 1Mb */  

} WordEntry;  

從WordEntry的定義可以看出PG中每個key的最大長度是2Kb,而每個TSVector的最大長度是1MB。

根據定義,TSVector的記憶體結構為:

[vl_len_] [size] [entry array] {[lexeme][pos num][pos array], … , [lexeme][pos num][pos array]}  

對於TEXT型別,在更新索引時,會先呼叫to_tsvector把基表的索引列的字串轉換成TSVector表示的key和位置列表的集合,然後在使用使用者自定義的extractValue把TSVector中所有的key提取出來,對每個key建立一個索引元組,然後插入到GIN索引中。

TSQuery

TSQuery用來表示全文搜尋的查詢,PG提供了一個to_tsquery和plainto_tsquery介面把待查詢的字串格式化成TSQuery,其結構如下:

typedef struct  

{  

         int32                  vl_len_;  

         int4           size;                    /* number of QueryItems */  

         char          data[1];  

} TSQueryData;  

其中QueryItem的結構為:

typedef union  

{  

         QueryItemType type;  

         QueryOperator qoperator;  

         QueryOperand qoperand;  

} QueryItem;  

QueryItem是一個操作符和運算元的聯合體,對於to_tsquery中的每一項都會轉換成一個QueryItem。

在GIN掃描之前會把TSQuery中的key使用函式extractQuery提取出來,併為每個key建立一個GinScanEntry。在GIN掃描時,會對每個GinScanKey呼叫consistent介面根據TSQuery中記錄的key之間的關係(&、|、!)合併每個GinScanEntry的結果集。

參考

src/backend/access/gin/README

src/backend/access/gin/*

src/include/access/gin*

《PostgreSQL資料庫核心分析》 ( 成書較早,大量內容基於8.4的程式碼編寫 )

http://zisedeqing.blog.163.com/blog/static/95550871201621623458216/

《深入淺出PostgreSQL B-Tree索引結構》

《B-Tree和B+Tree》


相關文章