若相關索引已經以所需的順序交出列,帶 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 |
---------------------------------------------------------------

診斷:為什麼還在排序?#

當你期待管線化執行、資料庫卻仍然排序時,只有兩個可能原因:

  1. 帶明確排序的執行計畫成本值更好
  2. 被掃描範圍內的索引順序與 order by 子句不一致

這等於把查詢調整成配合索引,排除了第二個原因。若資料庫仍然明確排序,就是最佳化工具基於成本偏好該計畫;否則就是索引無法用於原本的 order by 子句。

執行「完整索引定義版」的查詢並檢視結果,你往往會發現:自己對索引順序的認知是錯的,索引順序確實不符合原本 order by 的要求,資料庫因而無法用它省下排序。

若是最佳化工具基於成本而偏好明確排序,通常是因為它針對「完整執行整段查詢」挑最佳計畫——換言之,它選的是「取得最後一筆記錄最快」的計畫。若資料庫察覺應用程式只抓前面幾列,它反而可能改選索引化的 order by。第 7 章「部分結果」會說明對應的優化方法。

延伸:自動最佳化的叢集因子

Oracle 透過把 ROWID 納入索引順序,把叢集因子維持在最低:當兩個索引條目的鍵值相同時,由 ROWID 決定最終順序。由於 ROWID 代表資料表列的實體位址,索引因而也順著資料表順序排列,叢集因子降到可能的最小值。

為索引新增一個欄位,等於在 ROWID 之前插入一個新的排序條件。資料庫依資料表順序對齊索引條目的自由度變小,索引叢集因子只會變差

儘管如此,索引順序仍可能大致對應資料表順序:同一天的銷售在表中與索引中大概仍聚在一起——即使先後次序不再完全相同。使用 SALE_DT_PR 索引時,資料庫必須多次讀取資料表區塊,但那些就是原本那些區塊。拜常用資料的快取所賜,實際效能衝擊可能遠低於成本值所暗示的程度。