資料庫中儲存的資料量對效能影響極大。「查詢會隨著資料變多而變慢」通常已是共識,但——資料量翻倍時,效能衝擊有多大?我們又能如何改善這個比率? 這才是討論資料庫擴展性的關鍵問題。

一個對照實驗#

我們分析以下查詢在兩種不同索引下的回應時間。索引定義暫時保密,會在討論過程中揭曉:

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_FASTSECTIONID2 必須佔據前兩位(兩者都作為存取述詞),但無法判斷它們的順序。測試所用的索引是:

CREATE INDEX scale_fast ON scale_data (section, id2, id1);

加上 ID1 純粹是為了讓這個索引與 SCALE_SLOW 大小相同——否則你可能會誤以為差異來自索引大小。