僅索引掃描(index-only scan)是所有調校手法中最強大的之一。它不只避免了「為評估 where 子句而存取資料表」,更能在索引中就找齊所有選出的欄位時,完全不碰資料表

覆蓋整段查詢#

要涵蓋整段查詢,索引必須包含 SQL 敘述中的所有欄位——特別是 select 子句的欄位

CREATE INDEX sales_sub_eur
    ON sales
     ( subsidiary_id, eur_value );

SELECT SUM(eur_value)
  FROM sales
 WHERE subsidiary_id = ?;

當然,索引 where 子句的優先度高於其他子句,所以 SUBSIDIARY_ID 放在第一位,才能作為存取述詞。

----------------------------------------------------------
| Id | Operation           | Name          | Rows  | Cost |
----------------------------------------------------------
| 0 | SELECT STATEMENT     |               |     1 |  104 |
| 1 | SORT AGGREGATE       |               |     1 |      |
|* 2 |    INDEX RANGE SCAN | SALES_SUB_EUR | 40388 |  104 |
----------------------------------------------------------

執行計畫中只有索引掃描,沒有後續的 TABLE ACCESS BY INDEX ROWID

若一個索引避免了資料表存取,它也被稱為覆蓋索引(covering index)。但這個說法有誤導性——它聽起來像索引的某種屬性。「僅索引掃描」才正確地暗示了:這是一個執行計畫操作。

索引中有 EUR_VALUE 的副本,資料庫直接使用索引裡的值即可,不需要存取資料表。

效益取決於兩件事#

僅索引掃描可以帶來巨大的效能提升。看看上面的列數估算:最佳化工具預期要彙總超過 40,000 列——若每列各在不同的資料表區塊中,僅索引掃描就省下了 40,000 次資料表抓取

但若索引有良好的叢集因子(相關列都密集在少數幾個區塊中),優勢就會小得多。

被選出的列數同樣限制了潛在收益:若只選出一列,就只省下一次資料表存取;考慮到樹走訪本身也要讀取幾個區塊,這點節省可能微不足道。

僅索引掃描是激進的索引策略。不要僅憑臆測就為它設計索引——那會白白佔用記憶體,並增加 update 敘述的維護負擔(見第 8 章)。

實務上,先不考慮 select 子句來建索引,需要時再擴充

不愉快的意外#

僅索引掃描也可能帶來意外,例如把查詢限縮到近期的銷售:

SELECT SUM(eur_value)
  FROM sales
 WHERE subsidiary_id = ?
   AND sale_date > ?;

不看執行計畫的話,你會預期它更快——畢竟選出的列更少。但 where 子句參照了不在索引中的欄位,資料庫因此必須存取資料表來載入它:

--------------------------------------------------------------
|Id | Operation                    | Name       | Rows  |Cost |
--------------------------------------------------------------
| 0 | SELECT STATEMENT             |            |     1 | 371 |
| 1 | SORT AGGREGATE               |            |     1 |     |
|*2 | TABLE ACCESS BY INDEX ROWID  | SALES      |  2019 | 371 |
|*3 |    INDEX RANGE SCAN          | SALES_DATE | 10541 |  30 |
--------------------------------------------------------------

查詢選出的列更少,回應時間卻更長。

關鍵因素不是查詢交出多少列,而是資料庫為了找到它們必須檢視多少列。

擴充 where 子句可能造成「不合邏輯」的效能行為。擴充查詢前先檢查執行計畫。

最佳化工具的補救#

當索引不再能用於僅索引掃描時,最佳化工具會挑次好的計畫——可能是完全不同的計畫,也可能像上例那樣是換一個索引的類似計畫。這裡它用了上一章留下的 SALE_DATE 索引,理由有二:

  1. 它認為 SALE_DATE 的篩選更具選擇性(約 10,000 對 40,000 列)。但這些估算純屬武斷——查詢用了繫結參數,若傳入的是第一筆銷售的日期,SALE_DATE 條件可能選出整張表。
  2. 它有更好的叢集因子。這是站得住腳的理由:SALES 表只按時間順序成長,只要沒有列被刪除,新列永遠附加在表尾——表的順序與索引順序相符(兩者都大致依時間排序)。

叢集因子好的索引,被選出的資料表列存放得很密集,資料庫只需讀取少數幾個區塊就能取得全部列。這種情況下,即使沒有僅索引掃描,查詢也可能已經夠快——那我們就該把另一個索引中不必要的欄位移除。

有些索引天生就有良好的叢集因子,此時僅索引掃描的效能優勢極小

本例算是一個幸運的巧合:SALE_DATE 這個新篩選在阻斷僅索引掃描的同時,也打開了一條新的存取路徑,最佳化工具因此得以限制這項改動的效能衝擊。

但別指望總是這麼幸運。在其他子句加欄位同樣可能破壞僅索引掃描——而在 select 子句加欄位,永遠不可能打開新的存取路徑來緩衝失去僅索引掃描的代價。

函式索引的陷阱#

函式索引在僅索引掃描上也可能帶來不快:建在 UPPER(last_name) 上的索引,在選取 LAST_NAME 欄位時無法用於僅索引掃描

回頭看上一節——我們其實應該索引 LAST_NAME 本身:既能支援 LIKE 篩選,又能在選取 LAST_NAME 時用於僅索引掃描。

避免對「無法作為存取述詞」的運算式建函式索引。

選欄位要克制#

像上面那種彙總查詢很適合僅索引掃描:它們查很多列、卻只要少數欄位,一個瘦索引就足以支援。查的欄位越多,為了支援僅索引掃描要塞進索引的欄位就越多。

各資料庫的索引大小限制

除了「索引很多列很佔空間」之外,你還可能撞上資料庫本身的限制。多數資料庫對「每個索引的欄位數」與「索引條目的總大小」都有相當硬性的限制。

MySQL

  • MySQL 5.6 搭配 InnoDB:單一欄位上限 767 bytes,所有欄位合計 3072 bytes。MyISAM 索引上限 16 欄、鍵長 1000 bytes。

  • MySQL 有一項獨特功能稱為前綴索引(prefix indexing,有時也叫 partial indexing):只索引欄位的前幾個字元——這與第 2 章的部分索引無關。若索引的欄位超過允許長度,MySQL 會自動截斷,並以警告「Specified key was too long; max key length is 767 bytes」告知。這代表索引不再持有欄位的完整副本,因而對僅索引掃描的用處有限(類似函式索引)。

  • 你也可以明確使用前綴索引來避開總鍵長限制:

    CREATE INDEX .. ON employees (last_name(10));

Oracle Database

  • 最大索引鍵長取決於區塊大小與索引儲存參數(約為區塊大小的 75% 減去一些開銷)。B-tree 索引上限 32 欄。
  • Oracle 11g 全預設(8k 區塊)時,最大鍵長為 6398 bytes;超過會拋出「ORA-01450: maximum key length (6398) exceeded」。

PostgreSQL

  • 自 9.2 起支援僅索引掃描。
  • B-tree 索引鍵長上限 2713 bytes(寫死,約 BLCKSZ/3)。對應的錯誤訊息「index row size … exceeds btree maximum, 2713」只在執行超限的 insert 或 update 時才出現。B-tree 索引最多 32 欄。

SQL Server

  • 鍵長上限 900 bytes、16 個鍵欄位。

  • 但 SQL Server 有一項功能,允許你把任意長度的欄位加進索引,專門用來支援僅索引掃描——它區分鍵欄位(key column)非鍵欄位(nonkey column):鍵欄位就是至今討論的索引欄位;非鍵欄位只存在葉節點中,可以任意長,但不能作為存取述詞(seek predicate)

    CREATE INDEX empsubupnam
        ON employees
          (subsidiary_id, last_name)
     INCLUDE(phone_number, first_name);