僅索引掃描僅憑索引中的冗餘資料就執行完整段 SQL,堆積表(heap table)中的原始資料根本用不上。若把這個概念再推一步、把所有欄位都放進索引,你可能會想:那還需要堆積表嗎?

有些資料庫確實能把索引當作主要的資料表儲存。Oracle 稱之為索引組織表(IOT, index-organized table),其他資料庫則稱為叢集索引(clustered index)。本節視強調的對象是表還是索引,交替使用這兩個詞。

兩個誘人的好處#

索引組織表就是一個沒有堆積表的 B-tree 索引,帶來兩個好處:

  1. 省下堆積結構所佔的空間。
  2. 每一次對叢集索引的存取都自動是僅索引掃描

兩個好處聽起來都很誘人,但在實務上都很難兌現

缺點:次級索引沒有 ROWID#

當你在同一張表上建立另一個索引時,缺點就浮現了。這種所謂的**次級索引(secondary index)**同樣指向原始資料——但那份資料現在存在叢集索引裡,不像堆積表那樣靜態存放,隨時可能為了維持索引順序而搬動

因此次級索引無法存放實體位置,只能改用邏輯鍵

以「找出 2012 年 5 月 23 日的所有銷售」為例:

堆積表的情況——執行分兩步:INDEX RANGE SCANTABLE ACCESS BY INDEX ROWID

圖 5.1:堆積表上的索引存取

資料表存取雖可能成為瓶頸,但每列仍只限一次讀取操作:索引持有 ROWID 這個指向資料表列的直接指標,資料庫知道確切位置,可以立即載入。

索引組織表的情況——次級索引不存實體指標,只存叢集鍵(clustering key),通常就是該 IOT 的主鍵。

圖 5.2:索引組織表上的次級索引

存取次級索引得到的不是 ROWID,而是用來搜尋叢集索引的邏輯鍵;而單次存取不足以搜尋叢集索引——它需要一次完整的樹走訪

也就是說,透過次級索引存取資料表要搜尋兩個索引

  1. 次級索引一次(INDEX RANGE SCAN
  2. 對次級索引找到的每一列,再搜尋叢集索引一次(INDEX UNIQUE SCAN

透過次級索引存取索引組織表非常沒有效率。 叢集索引的 B-tree 橫亙在次級索引與資料之間。

緩解方式仍是僅索引掃描#

避免這種低效的方法,和避免堆積表的資料表存取相同:僅索引掃描——此處更精確的說法是「僅次級索引掃描」。而且效能優勢更大,因為它省下的不只是一次存取,而是整個 INDEX UNIQUE SCAN

資料庫會榨乾所有冗餘#

這個例子也顯示資料庫會善用它擁有的一切冗餘。記得次級索引為每個條目都存了叢集鍵——因此我們可以只從次級索引查出叢集鍵,完全不碰 IOT

SELECT sale_id
  FROM sales_iot
 WHERE sale_date = ?;
-------------------------------------------------
| Id | Operation         | Name           | Cost |
-------------------------------------------------
| 0 | SELECT STATEMENT   |                |    4 |
|* 1 | INDEX RANGE SCAN  | SALES_IOT_DATE |    4 |
-------------------------------------------------

SALES_IOTSALE_ID 為叢集鍵。雖然 SALES_IOT_DATE 索引只建在 SALE_DATE 上,它仍持有叢集鍵 SALE_ID 的副本,因此只用次級索引就能滿足查詢。

但一旦選取其他欄位,資料庫就得為每一列在叢集索引上跑一次 INDEX UNIQUE SCAN

SELECT eur_value
  FROM sales_iot
 WHERE sale_date = ?;
---------------------------------------------------
| Id | Operation           | Name           | Cost |
---------------------------------------------------
| 0 | SELECT STATEMENT     |                |   13 |
|* 1 | INDEX UNIQUE SCAN   | SALES_IOT_PK   |   13 |
|* 2 |    INDEX RANGE SCAN | SALES_IOT_DATE |    4 |
---------------------------------------------------

結論:不如乍看之下有用#

索引組織表與叢集索引,終究不如第一眼看上去那麼有用:

  • 叢集索引帶來的效能提升,一用次級索引就輕易賠光
  • 叢集鍵通常比 ROWID 長,次級索引因此比在堆積表上更大,往往抵消了省下堆積表的收益。
  • 只有一個索引的資料表,最適合實作為叢集索引或索引組織表。
  • 索引較多的資料表,往往從堆積表獲益更多。你仍然可以用僅索引掃描來避開資料表存取——這既取得了叢集索引的查詢效能,又不拖慢其他索引

堆積表的優勢正在於:它提供一份固定不動的主副本,可以被輕易參照。

延伸:為什麼次級索引沒有 ROWID

次級索引當然也想要一個指向資料表列的直接指標,但那只有在資料表列停留在固定儲存位置時才可能

若該列本身是「必須維持順序」的索引結構的一部分,這就辦不到了:維持索引順序偶爾就得搬動列——即使是不影響該列本身的操作也一樣。例如一次 insert 可能為了替新條目騰出空間而分裂葉節點,導致部分條目被搬到別處的新資料區塊。

堆積表則不維持任何列的順序:資料庫在任何找得到足夠空間的地方寫入新條目。一旦寫入,堆積表中的資料就不再移動。

各資料庫的支援情況

資料庫對索引組織表與叢集索引的支援極不一致。

MySQL:MyISAM 引擎只用堆積表,InnoDB 引擎一律使用叢集索引——你沒有直接的選擇權

Oracle Database:預設使用堆積表。可用 ORGANIZATION INDEX 子句建立索引組織表:

CREATE TABLE (
   id    NUMBER NOT NULL PRIMARY KEY,
   [...]
) ORGANIZATION INDEX;

Oracle 一律以主鍵作為叢集鍵

PostgreSQL:只使用堆積表。不過可以用 CLUSTER 子句讓堆積表的內容與某個索引對齊。

SQL Server:預設使用叢集索引(索引組織表),以主鍵為叢集鍵;但你可以指定任意欄位當叢集鍵——甚至是非唯一的欄位。要建立堆積表,必須在主鍵定義中使用 NONCLUSTERED

CREATE TABLE (
   id    NUMBER NOT NULL,
   [...]
   CONSTRAINT pk PRIMARY KEY NONCLUSTERED (id)
);

刪除叢集索引會把該表轉為堆積表。