在上一篇文章,索引扫描路径之1:Index Unique Scan 中,详细描述了Oracle的CBO优化器会在何种情形下选择通过Index Unique Scan的方式来访问数据。
本篇描述CBO优化器会在什么场景下选择Index Range Scan的路径来访问数据?
Index Range Scan通常见于通过索引去查找具有高选择性的数据。默认情况下,Oracle是按照被索引字段升序排序来存放索引记录的,如果被索引字段有重复值,则按照相应的ROWID做升序排序来存放。
较为常见的Index Range Scan的WHERE条件如下:
Col1=:b1
Col1>:b1
Col1>=:b1
Col1<:b1
Col1<=:b1
或者是通过AND连接的满足上述可构成前导列的条件。再或者WHERE条件中有使用BETWEEN…AND条件。
同样,给出验证与说明:
系统版本、数据库版本和平台,以及测试表同与之前的环境。即选择Linux X86_64平台上的一套11.2.0.1.0库,使用HR这个schema下的departments表。
1 唯一索引,CBO选择Index Range Scan的场景:
SQL> set autotrace trace
SQL> select * from departments where department_id>260;
Execution Plan
----------------------------------------------------------
Plan hash value: 3346631158
-------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
-------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 20 | 2 (0)| 00:00:01 |
| 1 | TABLE ACCESS BY INDEX ROWID| DEPARTMENTS | 1 | 20 | 2 (0)| 00:00:01 |
|* 2 | INDEX RANGE SCAN | DEPT_ID_PK | 1 | | 1 (0)| 00:00:01 |
-------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
2 - access("DEPARTMENT_ID">260)
Statistics
----------------------------------------------------------
0 recursive calls
0 db block gets
2 consistent gets
0 physical reads
0 redo size
743 bytes sent via SQL*Net to client
492 bytes received via SQL*Net from client
2 SQL*Net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
1 rows processed
SQL>
从上,即使是从唯一索引中获取一条数据[满足department_id>260条件的记录,确实只有1条],优化器也选择了走INDEX RANGE SCAN,因为上述SQL并不满足Index Unique Scan的条件。恰恰满足了INDEX RANGE SCAN 的条件。
2 接下来,一个典型的使用INDEX RANGE SCAN 的场景:
SQL> select * from departments where location_id=1700;
21 rows selected.
Execution Plan
----------------------------------------------------------
Plan hash value: 735461860
------------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 21 | 420 | 2 (0)| 00:00:01 |
| 1 | TABLE ACCESS BY INDEX ROWID| DEPARTMENTS | 21 | 420 | 2 (0)| 00:00:01 |
|* 2 | INDEX RANGE SCAN | DEPT_LOCATION_IX | 21 | | 1 (0)| 00:00:01 |
------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
2 - access("LOCATION_ID"=1700)
Statistics
----------------------------------------------------------
1 recursive calls
0 db block gets
6 consistent gets
1 physical reads
0 redo size
1468 bytes sent via SQL*Net to client
503 bytes received via SQL*Net from client
3 SQL*Net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
21 rows processed
SQL>
或者:
SQL> select * from departments where location_id<=1700;
23 rows selected.
Execution Plan
----------------------------------------------------------
Plan hash value: 735461860
------------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 24 | 480 | 2 (0)| 00:00:01 |
| 1 | TABLE ACCESS BY INDEX ROWID| DEPARTMENTS | 24 | 480 | 2 (0)| 00:00:01 |
|* 2 | INDEX RANGE SCAN | DEPT_LOCATION_IX | 24 | | 1 (0)| 00:00:01 |
------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
2 - access("LOCATION_ID"<=1700)
Statistics
----------------------------------------------------------
0 recursive calls
0 db block gets
6 consistent gets
0 physical reads
0 redo size
1502 bytes sent via SQL*Net to client
503 bytes received via SQL*Net from client
3 SQL*Net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
23 rows processed
SQL>
再或者:
SQL> select * from departments where location_id>=1700;
25 rows selected.
Execution Plan
----------------------------------------------------------
Plan hash value: 735461860
------------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 25 | 500 | 2 (0)| 00:00:01 |
| 1 | TABLE ACCESS BY INDEX ROWID| DEPARTMENTS | 25 | 500 | 2 (0)| 00:00:01 |
|* 2 | INDEX RANGE SCAN | DEPT_LOCATION_IX | 25 | | 1 (0)| 00:00:01 |
------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
2 - access("LOCATION_ID">=1700)
Statistics
----------------------------------------------------------
1 recursive calls
0 db block gets
6 consistent gets
0 physical reads
0 redo size
1561 bytes sent via SQL*Net to client
503 bytes received via SQL*Net from client
3 SQL*Net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
25 rows processed
SQL>
可以看到,不管是location_id>=1700还是location_id=1700或者是location_id<=1700条件,CBO都选择了location_id字段上的索引DEPT_LOCATION_IX通过INDEX RANGE SCAN 的方式去获取数据。
3 接下来,BETWEEN…AND的场景:
SQL> select department_id,location_id,rowid from departments where location_id between 1500 and 2600;
25 rows selected.
Execution Plan
----------------------------------------------------------
Plan hash value: 735461860
------------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 26 | 494 | 2 (0)| 00:00:01 |
| 1 | TABLE ACCESS BY INDEX ROWID| DEPARTMENTS | 26 | 494 | 2 (0)| 00:00:01 |
|* 2 | INDEX RANGE SCAN | DEPT_LOCATION_IX | 26 | | 1 (0)| 00:00:01 |
------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
2 - access("LOCATION_ID">=1500 AND "LOCATION_ID"<=2600)
Statistics
----------------------------------------------------------
1 recursive calls
0 db block gets
6 consistent gets
0 physical reads
0 redo size
1556 bytes sent via SQL*Net to client
503 bytes received via SQL*Net from client
3 SQL*Net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
25 rows processed
SQL>
那么,如果是通过department_id字段来做BETWEEN…AND条件测试呢?即测试在一个唯一性索引上使用BETWEEN…AND条件的情形。
SQL> select department_id,location_id,rowid from departments where department_id between 120 and 130;
Execution Plan
----------------------------------------------------------
Plan hash value: 3346631158
-------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
-------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 3 | 57 | 2 (0)| 00:00:01 |
| 1 | TABLE ACCESS BY INDEX ROWID| DEPARTMENTS | 3 | 57 | 2 (0)| 00:00:01 |
|* 2 | INDEX RANGE SCAN | DEPT_ID_PK | 3 | | 1 (0)| 00:00:01 |
-------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
2 - access("DEPARTMENT_ID">=120 AND "DEPARTMENT_ID"<=130)
Statistics
----------------------------------------------------------
1 recursive calls
0 db block gets
4 consistent gets
0 physical reads
0 redo size
748 bytes sent via SQL*Net to client
492 bytes received via SQL*Net from client
2 SQL*Net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
2 rows processed
SQL>
测试结果发现,即使是唯一索引,CBO依然会选择INDEX RANGE SCAN。
反之,如果条件换做是department_id between 120 and 120的话,很明显,聪明的CBO这次肯定会选择Index Unique Scan。
SQL> select department_id,location_id,rowid from departments where department_id between 120 and 120;
Execution Plan
----------------------------------------------------------
Plan hash value: 4024094692
-------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
-------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 19 | 1 (0)| 00:00:01 |
| 1 | TABLE ACCESS BY INDEX ROWID| DEPARTMENTS | 1 | 19 | 1 (0)| 00:00:01 |
|* 2 | INDEX UNIQUE SCAN | DEPT_ID_PK | 1 | | 0 (0)| 00:00:01 |
-------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
2 - access("DEPARTMENT_ID"=120)
Statistics
----------------------------------------------------------
0 recursive calls
0 db block gets
2 consistent gets
0 physical reads
0 redo size
673 bytes sent via SQL*Net to client
492 bytes received via SQL*Net from client
2 SQL*Net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
1 rows processed
SQL>
4 最后,看一个关于INDEX RANGE SCAN DESCENDING的场景:
CBO选择INDEX RANGE SCAN DESCENDING的方式来获取数据,适用于 ORDER BY column_name DESC的场景。在默认情况下,索引是按照索引关键字升序来存放数据的。
按照常理来讲,如果有ORDER BY column_name DESC的条件,那么
① 需要先从索引中读取数据;
② 再按照条件中column_name字段做降序排序。
而选择INDEX RANGE SCAN DESCENDING,则是直接在索引上按照索引关键字降序查找数据,这样正是为了避免先按照索引来查找数据,然后再做一次降序排序的操作。
SQL> set autotrace on
SQL> select department_id,location_id,rowid from departments where location_id between 1500 and 2600 order by location_id desc;
DEPARTMENT_ID LOCATION_ID ROWID
------------- ----------- ------------------
80 2500 AAAz+PABRAAAACbAAH
40 2400 AAAz+PABRAAAACbAAD
20 1800 AAAz+PABRAAAACbAAB
270 1700 AAAz+PABRAAAACbAAa
260 1700 AAAz+PABRAAAACbAAZ
250 1700 AAAz+PABRAAAACbAAY
240 1700 AAAz+PABRAAAACbAAX
230 1700 AAAz+PABRAAAACbAAW
220 1700 AAAz+PABRAAAACbAAV
210 1700 AAAz+PABRAAAACbAAU
200 1700 AAAz+PABRAAAACbAAT
DEPARTMENT_ID LOCATION_ID ROWID
------------- ----------- ------------------
190 1700 AAAz+PABRAAAACbAAS
180 1700 AAAz+PABRAAAACbAAR
170 1700 AAAz+PABRAAAACbAAQ
160 1700 AAAz+PABRAAAACbAAP
150 1700 AAAz+PABRAAAACbAAO
140 1700 AAAz+PABRAAAACbAAN
130 1700 AAAz+PABRAAAACbAAM
120 1700 AAAz+PABRAAAACbAAL
110 1700 AAAz+PABRAAAACbAAK
100 1700 AAAz+PABRAAAACbAAJ
90 1700 AAAz+PABRAAAACbAAI
DEPARTMENT_ID LOCATION_ID ROWID
------------- ----------- ------------------
30 1700 AAAz+PABRAAAACbAAC
10 1700 AAAz+PABRAAAACbAAA
50 1500 AAAz+PABRAAAACbAAE
25 rows selected.
Execution Plan
----------------------------------------------------------
Plan hash value: 2689299823
-------------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
-------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 26 | 494 | 2 (0)| 00:00:01 |
| 1 | TABLE ACCESS BY INDEX ROWID | DEPARTMENTS | 26 | 494 | 2 (0)| 00:00:01 |
|* 2 | INDEX RANGE SCAN DESCENDING| DEPT_LOCATION_IX | 26 | | 1 (0)| 00:00:01 |
-------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
2 - access("LOCATION_ID">=1500 AND "LOCATION_ID"<=2600)
Statistics
----------------------------------------------------------
0 recursive calls
0 db block gets
6 consistent gets
0 physical reads
0 redo size
1575 bytes sent via SQL*Net to client
534 bytes received via SQL*Net from client
3 SQL*Net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
25 rows processed
SQL>
从上可以看到,对于LOCATION_ID=1700的记录,则正是按照ROWID做的降序排序操作。