本資料庫中的所有索引是二級索引,這意味著每個索引都是與表的主要資料區(在本資料庫術語稱為表的堆中)分開儲存。這意味著在普通索引掃描中,每行檢索都需要從索引和堆中取資料。 此外,雖然匹配給定的可索引WHERE條件的索引條目通常在一起靠近儲存,但它們引用的錶行可能在堆中的任何地方。 因此索引掃描的堆訪問部分涉及到對堆的大量隨機訪問,這可能很慢,特別是在傳統旋轉媒介上。
為瞭解決這種效能問題,本資料庫支援只用索引的掃描,這類掃描可以僅用一個索引來回答查詢而不產生任何堆訪問。其基本思想是直接從每一個索引項目中直接傳回值,而不是去參考相關的堆項。在使用這種方法時有兩個根本的限制:
索引類型必須支援只用索引的掃描。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;
```
但是這些查詢不能使用只用索引的查詢:
```sql
SELECT x, z FROM tab WHERE x = 'key';
SELECT x FROM tab WHERE x = 'key' AND z < 42;
```
(如下面所討論的,運算式索引和部分索引會讓這條規則更加複雜)。
如果符合這兩個根本要求,那麼該查詢所要求的所有資料值都可以從索引得到,因此才可能使用只用索引的掃描。但是對本資料庫中的任何錶掃描還有一個額外的要求:必須驗證每一個檢索到的行對該查詢的 MVCC 快照是<span class="quote">“<span class="quote">可見的</span>”</span>。可見度資訊並不儲存在索引項目中,只儲存在堆項中。因此,乍一看似乎每一次行檢索無論如何都會要求一次堆訪問。如果錶行最近被修改過,確實是這樣。但是,對於很少更改的資料有一種方法可以解決這個問題。本資料庫為表堆中的每一個頁面跟蹤是否其中所有的行的年齡都足夠大,以至於對所有當前以及未來的事務都可見。這個資訊儲存在該表的*可見度映射*的一個位中。在找到一個候選索引項目後,只用索引的掃描會檢查對應堆頁面的可見度映射位。如果該位被設定,那麼這一行就是可見的並且該資料庫可以直接被返回。如果該位沒有被設定,那麼就必須訪問堆項以確定這一行是否可見,這種情況下相對於標準索引掃描就沒有效能優勢。即便是在成功的情況下,這種方法也是把對堆的訪問換成了對可見度映射的訪問。不過由於可見度映射比它所描述的堆要小四個數量級,所以訪問可見度映射所需的物理 I/O 要少很多。在大部分情況下,可見度映射總是會被保留在記憶體中的緩衝中。
總之,雖然當兩個根本要求滿足時可以使用只用索引的掃描,但是只有該表的堆頁面中有很大一部分的“所有都可見”映射位被設定時這種索引才有優勢。不過,有很大一部分行不被更改的表是很常見的,這也讓這一類掃描在實際中非常有用。
<span id="id-1.4.10.12.10.1" class="indexterm"></span> 為了有效利用僅索引掃描功能,您可以選擇建立一個*覆蓋索引*,它是一個特別設計的索引,包含經常啟動並執行特殊類型查詢所需要的列。由於查詢通常需要檢索的列不僅僅是他們搜尋的列,本資料庫允許您建立索引,這個索引中有些列只是<span class="quote">“<span class="quote">負荷</span>”</span>而不是搜尋鍵的一部分。這可以通過添加`INCLUDE`來完成子句來列出了額外的列。 例如,如果您通常可以運行這樣的查詢:
```sql
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 樹和 GiST 索引當前支援包含的列。
在 PostgreSQL有INCLUDE特性之前,人們有時會通過寫負載列作為普通索引列來製作覆蓋索引。它這樣寫:
CREATE INDEX tab_x_y ON tab(x, y);即使他們無意將y用作WHER子句的一部分,只要額外的列是尾列就可以很好的工作,讓它們成為前置欄位是不明智的。但是,此方法不支援您希望索引在鍵列上實施唯一性。Suffix truncation總是從 B-Tree 的上層移除非鍵列。作為承載列,它們從不用於指導索引掃描。 當鍵列的其餘首碼恰好足以描述最低 B-Tree 層級上的元組時,截斷過程還會刪除一個或多個尾隨的鍵列。 實際中,不使用INCLUDE子句覆蓋索引通常會避免儲存在上層承載的列。 然而,顯式地將承載列定義為非鍵列reliably使上層元組保持較小。原則上,只用索引的掃描可以被用於運算式索引。例如,給定一個f(x)上的索引(x是一個表列),可以把
SELECT f(x) FROM tab WHERE f(x) < 1;作為只用索引的掃描執行,如果f()是一個計算代價昂貴的函數,這會非常有吸引力。不過,本資料庫的規劃器當前面對這類情況時並不是很聰明。只有在索引中有查詢所需要的所有列時,規劃器才會考慮用只用索引的掃描來執行一個查詢。在這個例子中,除了在f(x)環境中之外,查詢的其他部分不需要x,但是規劃器並不能意識到這一點,因此它會得出不能使用只用索引的掃描的結論。如果只用索引的掃描足夠有價值,有一種解決方案是把該索引定義在(f(x), x)上,其中第二個列實際上並不會被使用,它只是用來說服規劃器可以使用只用索引的掃描而已。如果目標是避免重複計算f(x),一個額外的警示是規劃器不一定會把不在可索引WHERE子句中對f(x)的使用匹配到索引列。通常在上述那種簡單查詢中一切正常,但是涉及到串連的查詢中就不行了。這些不足將在未來的本資料庫版本中修正。部分索引也和只用索引的掃描之間有著有趣的關係。考慮以下所展示的部分索引:
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,因此在計劃中檢查這個部分的需要並不明顯。本資料庫 9.6 和以後的版本將會識別這種情況,並且允許產生只用索引的掃描,但是舊版本無法這樣做。