僅索引掃描僅憑索引中的冗餘資料就執行完整段 SQL,堆積表(heap table)中的原始資料根本用不上。若把這個概念再推一步、把所有欄位都放進索引,你可能會想:那還需要堆積表嗎?
有些資料庫確實能把索引當作主要的資料表儲存。Oracle 稱之為索引組織表(IOT, index-organized table),其他資料庫則稱為叢集索引(clustered index)。本節視強調的對象是表還是索引,交替使用這兩個詞。
兩個誘人的好處#
索引組織表就是一個沒有堆積表的 B-tree 索引,帶來兩個好處:
- 省下堆積結構所佔的空間。
- 每一次對叢集索引的存取都自動是僅索引掃描。
兩個好處聽起來都很誘人,但在實務上都很難兌現。
缺點:次級索引沒有 ROWID#
當你在同一張表上建立另一個索引時,缺點就浮現了。這種所謂的**次級索引(secondary index)**同樣指向原始資料——但那份資料現在存在叢集索引裡,不像堆積表那樣靜態存放,隨時可能為了維持索引順序而搬動。
因此次級索引無法存放實體位置,只能改用邏輯鍵。
以「找出 2012 年 5 月 23 日的所有銷售」為例:
堆積表的情況——執行分兩步:INDEX RANGE SCAN 與 TABLE ACCESS BY INDEX ROWID。

圖 5.1:堆積表上的索引存取
資料表存取雖可能成為瓶頸,但每列仍只限一次讀取操作:索引持有 ROWID 這個指向資料表列的直接指標,資料庫知道確切位置,可以立即載入。
索引組織表的情況——次級索引不存實體指標,只存叢集鍵(clustering key),通常就是該 IOT 的主鍵。

圖 5.2:索引組織表上的次級索引
存取次級索引得到的不是 ROWID,而是用來搜尋叢集索引的邏輯鍵;而單次存取不足以搜尋叢集索引——它需要一次完整的樹走訪。
也就是說,透過次級索引存取資料表要搜尋兩個索引:
- 次級索引一次(
INDEX RANGE SCAN)- 對次級索引找到的每一列,再搜尋叢集索引一次(
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_IOT 以 SALE_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)
);刪除叢集索引會把該表轉為堆積表。