若相關索引已經以所需的順序交出列,帶 order by 的查詢就不需要額外排序。這意味著:用於 where 子句的那個索引,必須同時涵蓋 order by 子句。
從明確排序到索引順序#
以「選出昨天的銷售,依銷售日期與產品 ID 排序」為例:
SELECT sale_date, product_id, quantity
FROM sales
WHERE sale_date = TRUNC(sysdate) - INTERVAL '1' DAY
ORDER BY sale_date, product_id;SALE_DATE 上已有索引可供 where 子句使用,但資料庫仍須為 order by 執行明確的排序:
---------------------------------------------------------------
|Id | Operation | Name | Rows | Cost |
---------------------------------------------------------------
| 0 | SELECT STATEMENT | | 320 | 18 |
| 1 | SORT ORDER BY | | 320 | 18 |
| 2 | TABLE ACCESS BY INDEX ROWID| SALES | 320 | 17 |
|*3 | INDEX RANGE SCAN | SALES_DATE | 320 | 3 |
---------------------------------------------------------------但 INDEX RANGE SCAN 本來就依索引順序交出結果。要利用這一點,只需把索引定義擴充成與 order by 子句一致:
DROP INDEX sales_date;
CREATE INDEX sales_dt_pr ON sales (sale_date, product_id);---------------------------------------------------------------
|Id | Operation | Name | Rows | Cost |
---------------------------------------------------------------
| 0 | SELECT STATEMENT | | 320 | 300 |
| 1 | TABLE ACCESS BY INDEX ROWID | SALES | 320 | 300 |
|*2 | INDEX RANGE SCAN | SALES_DT_PR | 320 | 4 |
---------------------------------------------------------------即使查詢仍有 order by 子句,SORT ORDER BY 已從計畫中消失——資料庫利用索引順序,跳過了明確的排序操作。
若索引順序與 order by 子句一致,資料庫就能省略明確的排序操作。
新計畫的操作雖然更少,成本值卻明顯上升——因為新索引的叢集因子變差了。這裡先記住一點:成本值不總是執行工作量的好指標。
只要「被掃描的索引範圍」有序就夠了#
這項優化的成立條件,只是被掃描的索引範圍依 order by 子句排序即可。因此就本例而言,只按 PRODUCT_ID 排序同樣有效:
SELECT sale_date, product_id, quantity
FROM sales
WHERE sale_date = TRUNC(sysdate) - INTERVAL '1' DAY
ORDER BY product_id;
圖 6.1:相關索引範圍中的排序
在被掃描的範圍內,PRODUCT_ID 是唯一相關的排序條件。
由於該範圍內索引順序與 order by 一致,資料庫得以省略排序。
擴大掃描範圍會破壞它#
這項優化在擴大掃描範圍時可能造成意外行為:
SELECT sale_date, product_id, quantity
FROM sales
WHERE sale_date >= TRUNC(sysdate) - INTERVAL '1' DAY
ORDER BY product_id;這段查詢取的不是「昨天的銷售」,而是「昨天以來的所有銷售」——涵蓋多天,掃描的索引範圍不再只依 PRODUCT_ID 排序。把圖 6.1 的範圍往下延伸就會看到:又出現了更小的 PRODUCT_ID 值。資料庫因此必須重新使用明確排序:
---------------------------------------------------------------
|Id |Operation | Name | Rows | Cost |
---------------------------------------------------------------
| 0 |SELECT STATEMENT | | 320 | 301 |
| 1 | SORT ORDER BY | | 320 | 301 |
| 2 | TABLE ACCESS BY INDEX ROWID| SALES | 320 | 300 |
|*3 | INDEX RANGE SCAN | SALES_DT_PR | 320 | 4 |
---------------------------------------------------------------診斷:為什麼還在排序?#
當你期待管線化執行、資料庫卻仍然排序時,只有兩個可能原因:
- 帶明確排序的執行計畫成本值更好。
- 被掃描範圍內的索引順序與 order by 子句不一致。
這等於把查詢調整成配合索引,排除了第二個原因。若資料庫仍然明確排序,就是最佳化工具基於成本偏好該計畫;否則就是索引無法用於原本的 order by 子句。
執行「完整索引定義版」的查詢並檢視結果,你往往會發現:自己對索引順序的認知是錯的,索引順序確實不符合原本 order by 的要求,資料庫因而無法用它省下排序。
若是最佳化工具基於成本而偏好明確排序,通常是因為它針對「完整執行整段查詢」挑最佳計畫——換言之,它選的是「取得最後一筆記錄最快」的計畫。若資料庫察覺應用程式只抓前面幾列,它反而可能改選索引化的 order by。第 7 章「部分結果」會說明對應的優化方法。
延伸:自動最佳化的叢集因子
Oracle 透過把 ROWID 納入索引順序,把叢集因子維持在最低:當兩個索引條目的鍵值相同時,由 ROWID 決定最終順序。由於 ROWID 代表資料表列的實體位址,索引因而也順著資料表順序排列,叢集因子降到可能的最小值。
為索引新增一個欄位,等於在 ROWID 之前插入一個新的排序條件。資料庫依資料表順序對齊索引條目的自由度變小,索引叢集因子只會變差。
儘管如此,索引順序仍可能大致對應資料表順序:同一天的銷售在表中與索引中大概仍聚在一起——即使先後次序不再完全相同。使用 SALE_DT_PR 索引時,資料庫必須多次讀取資料表區塊,但那些就是原本那些區塊。拜常用資料的快取所賜,實際效能衝擊可能遠低於成本值所暗示的程度。