資料庫中儲存的資料量對效能影響極大。「查詢會隨著資料變多而變慢」通常已是共識,但——資料量翻倍時,效能衝擊有多大?我們又能如何改善這個比率? 這才是討論資料庫擴展性的關鍵問題。
一個對照實驗#
我們分析以下查詢在兩種不同索引下的回應時間。索引定義暫時保密,會在討論過程中揭曉:
SELECT count(*)
FROM scale_data
WHERE section = ?
AND id2 = ?SECTION 欄位在此有特殊用途:它控制資料量——SECTION 數字越大,查詢選出的列越多。

圖 3.1:效能比較——「快」0.029 秒 vs.「慢」0.055 秒
兩種索引方案之間有明顯的效能差距,但兩者都遠低於十分之一秒,即使較慢的那個在多數情況下大概也夠快了。
然而這張圖只顯示一個測試點。談擴展性,就是要看改變環境參數(例如資料量)時的效能影響。
擴展性呈現的是效能對資料量等因素的依賴關係;單一效能數值只是擴展性圖表上的一個資料點。

圖 3.2:依資料量的擴展性
隨著資料量成長,兩個索引的回應時間都在上升。但在圖的右側(資料量為原本的一百倍時):
- 快的查詢:耗時略多於原本的兩倍。
- 慢的查詢:回應時間暴增 20 倍,超過一秒。
SQL 查詢的回應時間取決於許多因素,資料量只是其中之一。在某些測試條件下夠快,不代表在正式環境也夠快——尤其當開發環境的資料量只有正式系統的一小部分時。
差距從何而來:看述詞資訊#
資料多了查詢變慢並不意外,但兩個索引之間如此驚人的落差就出乎意料了。比較執行計畫應該能找出原因:
------------------------------------------------------
| Id | Operation | Name | Rows | Cost |
------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 972 |
| 1 | SORT AGGREGATE | | 1 | |
|* 2 | INDEX RANGE SCAN | SCALE_SLOW | 3000 | 972 |
------------------------------------------------------
------------------------------------------------------
| Id | Operation | Name | Rows | Cost |
------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 13 |
| 1 | SORT AGGREGATE | | 1 | |
|* 2 | INDEX RANGE SCAN | SCALE_FAST | 3000 | 13 |
------------------------------------------------------兩個計畫幾乎相同,只是用了不同索引。成本值反映了速度差異,但原因在計畫中看不出來。
這看起來又像「慢索引體驗」:用了索引卻很慢。但我們已經不再相信「索引壞掉」的迷思,而是想起讓索引查找變慢的兩個成分:資料表存取、掃描過寬的索引範圍。兩個計畫都沒有 TABLE ACCESS BY INDEX ROWID,所以一定是其中一個掃了更寬的索引範圍。
那執行計畫在哪裡顯示掃描範圍?當然是在述詞資訊裡。
補上述詞資訊後,差異一目了然:
|* 2 | INDEX RANGE SCAN | SCALE_SLOW | 3000 | 972 |
Predicate Information:
2 - access("SECTION"=TO_NUMBER(:A))
filter("ID2"=TO_NUMBER(:B))
|* 2 | INDEX RANGE SCAN | SCALE_FAST | 3000 | 13 |
Predicate Information:
2 - access("SECTION"=TO_NUMBER(:A) AND "ID2"=TO_NUMBER(:B))SCALE_SLOW:只有SECTION是存取述詞。資料庫讀取該 section 的所有列,再丟棄不符合ID2篩選述詞的那些。回應時間隨 section 中的列數成長。SCALE_FAST:所有條件都是存取述詞。回應時間隨被選出的列數成長。
從執行計畫反推索引定義#
拼圖的最後一塊是索引定義——我們能從執行計畫反推出來嗎?
SCALE_SLOW:定義必須以 SECTION 開頭(否則它無法作為存取述詞);而 ID2 上的條件不是存取述詞,所以它不能緊接在 SECTION 之後。因此該索引至少有三欄,SECTION 在第一位、ID2 不在第二位:
CREATE INDEX scale_slow ON scale_data (section, id1, id2);正是因為第二位的 ID1,資料庫無法把 ID2 當作存取述詞。
SCALE_FAST:SECTION 與 ID2 必須佔據前兩位(兩者都作為存取述詞),但無法判斷它們的順序。測試所用的索引是:
CREATE INDEX scale_fast ON scale_data (section, id2, id1);加上
ID1純粹是為了讓這個索引與SCALE_SLOW大小相同——否則你可能會誤以為差異來自索引大小。