PostgreSQL 中的所有索引都是輔助索引,這意味著每個索引都單獨儲存在表的主資料區域(在 PostgreSQL 術語中稱為表的堆)之外。這意味著在普通的索引掃描中,每次檢索行都需要從索引和堆中獲取資料。此外,雖然匹配給定可索引WHERE
條件的索引項通常在索引中是緊挨著的,但它們引用的錶行可能位於堆中的任何位置。因此,索引掃描的堆訪問部分涉及大量的隨機堆訪問,這可能很慢,尤其是在傳統的旋轉媒體上。(如11.5 節所述,點陣圖掃描試圖透過按排序順序進行堆訪問來減輕此成本,但這隻能在一定程度上緩解。)
為了解決這個效能問題,PostgreSQL 支援僅索引掃描,它可以僅從索引回答查詢,而無需任何堆訪問。基本思想是從每個索引項直接返回值,而不是查閱關聯的堆項。此方法的使用有兩個基本限制:
索引型別必須支援僅索引掃描。B-tree 索引始終支援。GiST 和 SP-GiST 索引對某些運算子類支援僅索引掃描,但對其他運算子類不支援。其他索引型別不支援。基本要求是索引必須物理儲存,或者能夠重建每個索引項的原始資料值。作為反例,GIN 索引不支援僅索引掃描,因為每個索引項通常只包含原始資料值的一部分。
查詢必須僅引用儲存在索引中的列。例如,給定一個表上具有列z
的列x
和y
的索引,這些查詢可以使用僅索引掃描:
SELECT x, y FROM tab WHERE x = 'key'; SELECT x FROM tab WHERE x = 'key' AND y < 42;
但這些查詢不能:
SELECT x, z FROM tab WHERE x = 'key'; SELECT x FROM tab WHERE x = 'key' AND z < 42;
(表示式索引和部分索引會使此規則複雜化,如下文所述。)
如果滿足這兩個基本要求,則查詢所需的所有資料值都可從索引中獲得,因此僅索引掃描在物理上是可能的。但是,PostgreSQL 中的任何表掃描都還有一個額外要求:它必須驗證每個檢索到的行對於查詢的 MVCC 快照是“可見”的,如第 13 章中所述。可見性資訊僅儲存在堆項中,而不是儲存在索引項中;因此,乍一看,似乎每次行檢索無論如何都需要堆訪問。而且,如果錶行最近被修改過,情況確實如此。然而,對於很少更改的資料,有一種方法可以解決此問題。PostgreSQL 會跟蹤表中堆的每個頁面,其中儲存的所有行是否足夠舊,以至於對所有當前和將來的事務都可見。此資訊儲存在表的可見性對映的一個位中。僅索引掃描在找到候選索引項後,會檢查相應堆頁面的可見性對映位。如果該位已設定,則該行已知可見,因此無需進一步工作即可返回資料。如果未設定該位,則必須訪問堆項以確定其是否可見,因此與標準索引掃描相比沒有效能優勢。即使在這種成功的情況下,這種方法也會用可見性對映訪問來代替堆訪問;但由於可見性對映比它描述的堆小四個數量級,因此訪問它所需的物理 I/O 要少得多。在大多數情況下,可見性對映會一直快取在記憶體中。
總之,雖然在滿足兩個基本要求的情況下可以進行僅索引掃描,但只有當表中很大一部分堆頁面的全部可見對映位都設定為 1 時,它才會有所收益。但是,資料行很大一部分不變的表很常見,這使得這種型別的掃描在實踐中非常有用。
為了有效利用僅索引掃描功能,您可以選擇建立一個覆蓋索引,這是一種專門設計的索引,用於包含您頻繁執行的特定型別查詢所需的列。由於查詢通常需要檢索比搜尋列更多的列,PostgreSQL 允許您建立一個索引,其中某些列只是“有效載荷”,而不是搜尋鍵的一部分。這是透過新增一個INCLUDE
子句來列出額外的列來完成的。例如,如果您經常執行如下查詢:
SELECT y FROM tab WHERE x = 'key';
加速此類查詢的傳統方法是僅在x
上建立索引。但是,定義為:
CREATE INDEX tab_x_y ON tab(x) INCLUDE (y);
的索引可以作為僅索引掃描處理這些查詢,因為可以從索引中獲取y
而無需訪問堆。
由於列y
不是索引搜尋鍵的一部分,因此它不必是索引可以處理的資料型別;它只是儲存在索引中,不被索引機制解釋。此外,如果該索引是唯一索引,即
CREATE UNIQUE INDEX tab_x_y ON tab(x) INCLUDE (y);
唯一性條件僅適用於列x
,而不適用於x
和y
的組合。(INCLUDE
子句也可以寫在UNIQUE
和PRIMARY KEY
約束中,提供設定此類索引的替代語法。)
明智的做法是謹慎地向索引新增非鍵有效載荷列,尤其是寬列。如果索引元組超過了索引型別允許的最大大小,則資料插入將失敗。無論如何,非鍵列會複製索引表中的資料,並會膨脹索引的大小,從而可能減慢搜尋速度。請記住,除非表更改緩慢到僅索引掃描可能不需要訪問堆,否則將有效載荷列包含在索引中幾乎沒有意義。如果必須訪問堆元組,則從那裡獲取列的值不會帶來額外成本。其他限制是表示式目前不支援作為包含的列,並且目前只有 B-tree、GiST 和 SP-GiST 索引支援包含的列。
在 PostgreSQL 擁有INCLUDE
功能之前,人們有時會透過將有效載荷列寫成普通索引列來建立覆蓋索引,即寫成:
CREATE INDEX tab_x_y ON tab(x, y);
即使他們從未打算將y
用作WHERE
子句的一部分。只要額外的列是尾隨列,這就可以正常工作;將它們作為前導列是不明智的,原因在11.3 節中解釋過。然而,這種方法不支援您希望索引強制對鍵列執行唯一性的情況。
字尾截斷始終會從上層 B-Tree 中刪除非鍵列。作為有效載荷列,它們從不用於指導索引掃描。截斷過程還會移除一個或多個尾隨鍵列,當剩餘的鍵列字首足以描述最低層 B-Tree 上的元組時。實際上,沒有INCLUDE
子句的覆蓋索引通常會避免在上層儲存有效載荷的列。然而,顯式地將有效載荷列定義為非鍵列可靠地使上層中的元組保持較小。
原則上,僅索引掃描可以與表示式索引一起使用。例如,給定一個在x
(表列)上的表示式索引f(x)
,應該可以執行
SELECT f(x) FROM tab WHERE f(x) < 1;
作為僅索引掃描;如果f()
是一個計算成本很高的函式,這會非常有吸引力。但是,PostgreSQL 的規劃器目前在這類情況下並不十分智慧。它僅在查詢所需的所有列都可以從索引中獲取時,才認為查詢可以由僅索引掃描執行。在此示例中,除了f(x)
的上下文之外,不需要x
,但規劃器沒有注意到這一點,並得出結論僅索引掃描是不可能的。如果僅索引掃描看起來足夠有價值,可以透過將x
新增為包含的列來解決,例如:
CREATE INDEX tab_f_x ON tab (f(x)) INCLUDE (x);
一個額外的警告是,如果目標是避免重新計算f(x)
,規劃器不一定會將不在可索引WHERE
子句中的f(x)
的使用與索引列匹配。在上述簡單查詢中,它通常會正確處理,但在涉及連線的查詢中則不會。這些缺陷可能會在 PostgreSQL 的未來版本中得到修復。
部分索引也與僅索引掃描有有趣的互動。考慮示例 11.3中所示的部分索引:
CREATE UNIQUE INDEX tests_success_constraint ON tests (subject, target) WHERE success;
原則上,我們可以對該索引執行僅索引掃描以滿足類似以下查詢:
SELECT target FROM tests WHERE subject = 'some-subject' AND success;
但存在一個問題:WHERE
子句引用了success
,而它在索引的結果列中不可用。儘管如此,僅索引掃描是可能的,因為計劃在執行時不需要重新檢查WHERE
子句的該部分:索引中的所有條目必然滿足success = true
,因此無需在計劃中明確檢查。PostgreSQL 9.6 及更高版本將識別此類情況並允許生成僅索引掃描,但舊版本則不行。
如果您在文件中發現任何不正確的內容、與您對特定功能的體驗不符的內容,或需要進一步說明的內容,請使用此表單報告文件問題。