快轉到主要內容

SQL test 03 - OR Expansion (OR 展開) - 沒有 Index

·1797 字·4 分鐘·
PolloChang
作者
PolloChang
我是一隻雞

SQL test 03 - OR Expansion (OR 展開)
#

在 SQL Tuning 中,遇到 WHERE ... OR ... 的條件時,Oracle 優化器有時會啟動一種稱為 OR Expansion (OR 展開) 的查詢轉換機制。它的原理是將一個帶有 OR 的查詢,拆分成多個使用 UNION ALL 的獨立查詢,以期能分別使用不同的 Index 來提升效能。

這篇文章我們將手動模擬 OR Expansion 的過程,並看看在「沒有 Index」的情況下,優化器為什麼會做出不展開的決定。

測試情境
#

測試 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
┌───────────────────────────────────────────────────────────────────────────────────────────┐
│                                     PLAN TABLE OUTPUT                                     │
├───────────────────────────────────────────────────────────────────────────────────────────┤
│ Plan hash value: 4071893995                                                               │
│                                                                                           │
│ ----------------------------------------------------------------------------------------- │
│ | Id  | Operation          | Name               | Rows  | Bytes | Cost (%CPU)| Time     | │
│ ----------------------------------------------------------------------------------------- │
│ |   0 | SELECT STATEMENT   |                    |     1 |    11 |  3816   (1)| 00:00:01 | │
│ |   1 |  SORT AGGREGATE    |                    |     1 |    11 |            |          | │
│ |*  2 |   TABLE ACCESS FULL| USERS_DETAILS_2023 |   126K|  1364K|  3816   (1)| 00:00:01 | │
│ ----------------------------------------------------------------------------------------- │
│                                                                                           │
│ Predicate Information (identified by operation id):                                       │
│ ---------------------------------------------------                                       │
│                                                                                           │
│ 2 - filter("GENDER"=U'Male' OR "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: 1826431837                                                                │
│                                                                                            │
│ ------------------------------------------------------------------------------------------ │
│ | Id  | Operation           | Name               | Rows  | Bytes | Cost (%CPU)| Time     | │
│ ------------------------------------------------------------------------------------------ │
│ |   0 | SELECT STATEMENT    |                    |     2 |    18 |  7631   (1)| 00:00:01 | │
│ |   1 |  UNION-ALL          |                    |       |       |            |          | │
│ |   2 |   SORT AGGREGATE    |                    |     1 |     7 |            |          | │
│ |*  3 |    TABLE ACCESS FULL| USERS_DETAILS_2023 |     3 |    21 |  3815   (1)| 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 - filter("LOCATION"=U'Calgary, AB')                                                      │
│ 5 - filter("GENDER"=U'Male' AND LNNVL("LOCATION"=U'Calgary, AB'))                          │
└────────────────────────────────────────────────────────────────────────────────────────────┘

觀察與總結
#

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

  • SQL 1 (一般 OR):只進行了 1 次 TABLE ACCESS FULL,Cost 為 3816
  • SQL 2 (手動展開):進行了 2 次 TABLE ACCESS FULL,Cost 疊加為 3815 + 3816 = 7631

Tuning 核心觀念
#

可以看到,手動展開後 SQL 2 的 Cost 足足是 SQL 1 的兩倍!為什麼會這樣?

這是因為本測試中,LOCATIONGENDER 欄位都沒有建立 Index。 如果沒有 Index,每一次的查詢分支都必須執行一次極度消耗資源的「全表掃描 (Full Table Scan)」。將條件展開成兩段 UNION ALL,就意味著資料庫要把整張大表從頭到尾掃描兩遍。

Oracle 的 Cost-Based Optimizer (CBO) 非常聰明,它在底層計算過成本後發現:「與其展開後掃描兩次大表,不如不展開,在一次全表掃描中同時判斷這兩個 OR 條件。」因此,優化器理智地不使用 OR Expansion。

結論OR Expansion 是一把雙面刃。它通常只在「OR 涉及的各個條件欄位都有適當的 Index」時,才能發揮威力。在缺乏 Index 的情況下,維持原來的 OR 寫法,讓資料庫一次掃描完成過濾,才是效能最佳的選擇。