快轉到主要內容

SQL test 04 - OR Expansion 與 INLIST ITERATOR (有 Index)

·2368 字·5 分鐘·
PolloChang
作者
PolloChang
我是一隻雞

SQL test 04 - OR Expansion 與 INLIST ITERATOR (有 Index)
#

在上一篇 [SQL test 03] 中我們看到了,如果查詢條件「沒有 Index」,Oracle 為了避免做兩次全表掃描,會拒絕執行 OR Expansion。 那麼,如果我們幫查詢條件加上了 Index,優化器又會怎麼做呢?

這篇文章我們將在「有 Index」的情況下,測試 WHERE ... OR ... 的效能與執行計畫。

建立 Index
#

先針對 LOCATIONGENDER 兩個欄位分別建立 B-Tree Index,以模擬優化器有 Index 可以利用的情境。

1
2
CREATE INDEX USERS_DETAILS_2023_IDX1 ON USERS_DETAILS_2023 (LOCATION);
CREATE INDEX USERS_DETAILS_2023_IDX2 ON USERS_DETAILS_2023 (GENDER);

測試情境
#

測試 SQL 1 (一般 OR 寫法)
#

這是一般開發者最直覺的寫法。

1
2
3
SELECT COUNT(*) 
FROM pollo.USERS_DETAILS_2023 
where LOCATION = 'Calgary, AB' or GENDER = 'Male';

執行結果:

1
2
3
4
5
┌───────────┐
│ COUNT (*) │
├───────────┤
│ 126990    │
└───────────┘

測試 SQL 2 (手動模擬 OR Expansion)
#

我們將原本的 OR 拆分成兩個獨立的查詢,並用 UNION ALL 組合。 為了避免兩邊的條件有交集而導致資料重複計算,Oracle 在內部做 OR Expansion 時,通常會自動加上 LNNVL 函數來排除已經在第一個條件成立的資料。

1
2
3
select count(*) from pollo.USERS_DETAILS_2023 where LOCATION = 'Calgary, AB' 
union all
select count(*) from pollo.USERS_DETAILS_2023 where GENDER = 'Male' and lnnvl(LOCATION = 'Calgary, AB');

執行結果:

1
2
3
4
5
6
┌───────────┐
│ COUNT (*) │
├───────────┤
│ 12        │
│ 126978    │
└───────────┘

(註:兩者相加 12 + 126978 剛好等於 SQL 1 的總數 126990)


執行計畫 (Explain Plan) 比較
#

SQL 1 的執行計畫 (未展開)
#

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│                                              PLAN TABLE OUTPUT                                               │
├──────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ Plan hash value: 2998711674                                                                                  │
│                                                                                                              │
│ ------------------------------------------------------------------------------------------------------------ │
│ | Id  | Operation                        | Name                    | Rows  | Bytes | Cost (%CPU)| Time     | │
│ ------------------------------------------------------------------------------------------------------------ │
│ |   0 | SELECT STATEMENT                 |                         |     1 |    11 |   392   (1)| 00:00:01 | │
│ |   1 |  SORT AGGREGATE                  |                         |     1 |    11 |            |          | │
│ |   2 |   BITMAP CONVERSION COUNT        |                         |   126K|  1364K|   392   (1)| 00:00:01 | │
│ |   3 |    BITMAP OR                     |                         |       |       |            |          | │
│ |   4 |     BITMAP CONVERSION FROM ROWIDS|                         |       |       |            |          | │
│ |*  5 |      INDEX RANGE SCAN            | USERS_DETAILS_2023_IDX2 |       |       |   389   (1)| 00:00:01 | │
│ |   6 |     BITMAP CONVERSION FROM ROWIDS|                         |       |       |            |          | │
│ |*  7 |      INDEX RANGE SCAN            | USERS_DETAILS_2023_IDX1 |       |       |     3   (0)| 00:00:01 | │
│ ------------------------------------------------------------------------------------------------------------ │
│                                                                                                              │
│ Predicate Information (identified by operation id):                                                          │
│ ---------------------------------------------------                                                          │
│                                                                                                              │
│ 5 - access("GENDER"=U'Male')                                                                                 │
│ 7 - access("LOCATION"=U'Calgary, AB')                                                                        │
└──────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

SQL 2 的執行計畫 (手動展開)
#

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
┌─────────────────────────────────────────────────────────────────────────────────────────────────┐
│                                        PLAN TABLE OUTPUT                                        │
├─────────────────────────────────────────────────────────────────────────────────────────────────┤
│ Plan hash value: 242963651                                                                      │
│                                                                                                 │
│ ----------------------------------------------------------------------------------------------- │
│ | Id  | Operation           | Name                    | Rows  | Bytes | Cost (%CPU)| Time     | │
│ ----------------------------------------------------------------------------------------------- │
│ |   0 | SELECT STATEMENT    |                         |     2 |    18 |  3819   (1)| 00:00:01 | │
│ |   1 |  UNION-ALL          |                         |       |       |            |          | │
│ |   2 |   SORT AGGREGATE    |                         |     1 |     7 |            |          | │
│ |*  3 |    INDEX RANGE SCAN | USERS_DETAILS_2023_IDX1 |     3 |    21 |     3   (0)| 00:00:01 | │
│ |   4 |   SORT AGGREGATE    |                         |     1 |    11 |            |          | │
│ |*  5 |    TABLE ACCESS FULL| USERS_DETAILS_2023      |   126K|  1364K|  3816   (1)| 00:00:01 | │
│ ----------------------------------------------------------------------------------------------- │
│                                                                                                 │
│ Predicate Information (identified by operation id):                                             │
│ ---------------------------------------------------                                             │
│                                                                                                 │
│ 3 - access("LOCATION"=U'Calgary, AB')                                                           │
│ 5 - filter("GENDER"=U'Male' AND LNNVL("LOCATION"=U'Calgary, AB'))                               │
└─────────────────────────────────────────────────────────────────────────────────────────────────┘

觀察與總結
#

從執行計畫中,我們可以清楚看到巨大的成本差異:

  • SQL 1 (一般 OR):使用了 BITMAP OR,Cost 為 392
  • SQL 2 (手動展開):使用了 INDEX RANGE SCAN 加上 TABLE ACCESS FULL,Cost 為 3819

Tuning 核心觀念
#

可以看到,手動展開成 UNION ALL 的 SQL 2 的 Cost 居然是 SQL 1 的將近 10 倍!為什麼會這樣?

在上一篇 [SQL test 03] 中我們學到,沒有 Index 時,拆成 UNION ALL 會導致做兩次全表掃描。 那這次我們明明在 LOCATIONGENDER 都加上了 Index,為什麼手動做 OR Expansion 的成本還是比較高呢?

這背後有兩個深刻的 Tuning 知識點:

1. 高成本的回表查詢 (Table Access By Index ROWID)
#

在 SQL 2 的第二個查詢中:GENDER = 'Male' AND LNNVL(LOCATION = 'Calgary, AB')。 雖然 GENDER 有 Index,但 Male 的資料量非常龐大 (約 12.6 萬筆)。如果走 Index,就必須透過 ROWID 回去原始資料表(回表)拿 LOCATION 欄位來做 LNNVL 驗證。這會產生高達十幾萬次的「隨機讀取 (Random I/O)」,成本遠比循序讀取還要高! 因此,聰明的優化器在第二個分支直接放棄使用 GENDER 的 Index,改採「全表掃描 (TABLE ACCESS FULL)」。這也導致 SQL 2 的總成本飆升到 3819。

2. 強大的 BITMAP OR (位元圖轉換)
#

針對 SQL 1,優化器找到了一條更棒的捷徑:Bitmap Conversion (位元圖轉換)。 既然 LOCATIONGENDER 都有獨立的 B-Tree Index,Oracle 可以將這兩個 Index 分別掃描出來的 ROWID,在記憶體中動態轉換成 Bitmap (位元圖)。接著直接對兩張 Bitmap 做 OR 邏輯運算(位元運算速度極快),最後再計算出總筆數(BITMAP CONVERSION COUNT)。 整個過程幾乎不需要去撈取龐大的原始資料表,因此 Cost 僅有 392,達到了極致的效能!

結論: 有了 Index 之後,Oracle 優化器依然會根據資料的分佈狀態(例如 Male 資料佔比極大、回表成本太高)來精算成本。在這個案例中,優化器完美運用了 BITMAP OR 的黑科技,秒殺了傳統的 UNION ALL 展開。 這再次呼應了我們的核心觀念:盡可能在過濾條件上提供足夠的「武器 (Index)」,然後信任資料庫強大的優化器 (CBO),通常就能得到最佳的執行路徑!

測試完成後動作
#

刪除 index

1
2
DROP INDEX USERS_DETAILS_2023_IDX1;
DROP INDEX USERS_DETAILS_2023_IDX2;