PostgreSQL 中的所有索引都是次要索引,這表示每個索引都與資料表的主要資料區域(在 PostgreSQL 術語中稱為資料表的堆積)分開儲存。這表示在一般的索引掃描中,每次資料列擷取都需要從索引和堆積中提取資料。此外,雖然符合給定可索引 WHERE
條件的索引條目通常在索引中靠得很近,但它們引用的資料表資料列可能位於堆積中的任何位置。因此,索引掃描的堆積存取部分涉及大量隨機存取堆積,這可能會很慢,尤其是在傳統旋轉媒體上。(如第 11.5 節中所述,位圖掃描嘗試通過按排序順序執行堆積存取來減輕此成本,但效果有限。)
為了解決此效能問題,PostgreSQL 支援僅索引掃描,它可以僅從索引回答查詢,而無需任何堆積存取。基本概念是直接從每個索引條目傳回值,而不是查詢關聯的堆積條目。對何時可以使用此方法有兩個基本限制
索引類型必須支援僅索引掃描。B 樹索引始終支援。GiST 和 SP-GiST 索引支援某些運算子類別的僅索引掃描,但不是全部。其他索引類型沒有支援。基本要求是索引必須實際儲存或能夠重建每個索引條目的原始資料值。作為一個反例,GIN 索引不能支援僅索引掃描,因為每個索引條目通常僅包含原始資料值的一部分。
查詢必須僅引用儲存在索引中的欄位。例如,給定一個資料表的欄位 x
和 y
上的索引,該資料表也具有欄位 z
,則以下查詢可以使用僅索引掃描
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 要少得多。在大多數情況下,可見性對應始終保留在記憶體中快取。
簡而言之,雖然給定兩個基本要求,可以進行僅索引掃描,但只有在資料表的大部分堆積頁面都已設定其全部可見對應位元時,它才會帶來好處。但是,其中大部分資料列都不會更改的資料表非常常見,因此這種掃描類型在實務中非常有用。
為了有效使用僅索引掃描功能,您可以選擇建立涵蓋索引,這是一種專門設計為包含您經常運行的特定類型查詢所需的欄位的索引。由於查詢通常需要檢索的欄位比它們搜尋的欄位更多,因此 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
約束中編寫,從而提供設置像這樣的索引的替代語法。)
對於在索引中加入非索引鍵酬載欄位(尤其是寬欄位),保持保守的態度是明智的。如果一個索引元組超過索引類型所允許的最大大小,資料插入將會失敗。無論如何,非索引鍵欄位會重複索引表中的資料,並膨脹索引的大小,從而可能減慢搜尋速度。請記住,除非資料表的變更速度夠慢,使索引掃描不太可能需要存取堆疊(heap),否則在索引中包含酬載欄位沒有太大意義。如果無論如何都必須存取堆疊元組,從堆疊中取得欄位的值並不會增加額外的成本。其他限制包括,目前不支援將表達式作為包含欄位,並且只有 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
子句的涵蓋索引通常會避免在上層儲存實際上是酬載的欄位。然而,明確將酬載欄位定義為非索引鍵欄位 可靠地 保持了上層元組的大小。
原則上,索引掃描可以與表達式索引一起使用。例如,給定一個關於 f(x)
的索引,其中 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
,而 success
並不能作為索引的結果欄位。儘管如此,仍然可以進行索引掃描,因為規劃不需要在執行階段重新檢查 WHERE
子句的這一部分:索引中找到的所有條目必然具有 success = true
,因此無需在規劃中明確檢查這一點。PostgreSQL 9.6 及更高版本將識別出這種情況,並允許生成索引掃描,但較舊的版本則不行。
如果您在文件中發現任何不正確、與您使用特定功能的經驗不符或需要進一步說明的地方,請使用此表單來回報文件問題。