Oracle索引梳理系列(八)- 索引扫描类型及分析(高效索引必备知识)

时间:2022-07-14 16:41:50

版权声明:本文发布于http://www.cnblogs.com/yumiko/,版权由Yumiko_sunny所有,欢迎转载。转载时,请在文章明显位置注明原文链接。若在未经作者同意的情况下,将本文内容用于商业用途,将保留追究其法律责任的权利。如果有问题,请以邮箱方式联系作者(793113046@qq.com)。


理解oracle索引扫描类型的特点以及具体触发的条件,对于通过合理地使用索引,进行sql优化至关重要(例如组合索引的引导列的选择问题)

在总结索引扫描类型前,需要再次强调关于索引特点的几个关键点:

  • 对于单一列建立的索引,既单一列索引,b-tree中不保存索引列的null值信息
  • 对于多个列建立的索引,既组合列索引,b-tree中会连同其他非null值列,保留该列null值记录;对于一条记录中,组合索引全部列都是null值,组合索引中不会记录(从之前的实验看,此时的执行计划是全表扫描)
  • 创建主键约束以及唯一键约束,或自动创建唯一索引
  • create index创建的索引属于普通索引(非唯一索引)
  • create unique index创建的索引属于唯一索引

其他的一些特点,可参阅前面的几篇总结。

此外,为避免概念的混淆,再次说明一下:“索引类型”主要探讨的是索引的几种类别的问题,而“索引扫描类型”主要探讨的是索引扫描的几种具体实现方法的问题。

1、索引扫描类型概述

Oracle提供了五种索引扫描类型,根据具体索引类型、数据分布、约束条件以及where限制的不同进行选择:

  • 索引唯一扫描(index unique scan)
  • 索引范围扫描(index range scan)
  • 索引跳跃扫描(index skip scan)
  • 索引全扫描(index full scan)
  • 索引快速扫描(index fast full scan)

2、索引唯一扫描(index unique scan)

索引唯一扫描,仅仅针对唯一索引的扫描,且仅适用于等值(=)条件的查询。从结果集看,至多返回一条记录。

具体情况分析:

  • 对于单一列建立的索引(单一索引),当索引属于唯一索引,在检索条件中,使用该索引进行检索,且检索值不是null时,会使用“索引唯一扫描”
  • 对于单一列建立的索引(单一索引),当索引属于唯一索引,在检索条件中,使用该索引进行检索,且检索值等于null时,会使用“全表扫描”
  • 对于多个列建立的索引(组合索引),当索引属于唯一索引,且检索条件中,使用该组合索引进行检索,且检索列使用组合索引涉及的所有列时,会使用“索引唯一扫描”

需要注意:

  • 对于组合索引,若其中涉及的部分列或则所有列,在同一条记录上存在null值。当使用完整的组合索引列作为检索条件,且使用该null值进行该条记录的检索时,不会使用“索引唯一扫描”

示例:

--创建唯一索引
Yumiko@Sunny >create unique index ind_test_normal on test_normal(empno,ENAME,SAL);
Index created. Yumiko@Sunny >select INDEX_NAME,INDEX_TYPE,TABLE_NAME,UNIQUENESS from user_indexes where TABLE_NAME='TEST_NORMAL'; INDEX_NAME INDEX_TYPE TABLE_NAME UNIQUENES
------------------------- --------------------------- --------------- ---------
IND_TEST_NORMAL NORMAL TEST_NORMAL UNIQUE --查询记录并查看执行计划
Yumiko@Sunny >select * from TEST_NORMAL where empno=7369 and ENAME='SMITH' and sal=800; EMPNO ENAME JOB SAL
---------- ---------- --------- ----------
7369 SMITH CLERK 800 Execution Plan
----------------------------------------------------------
Plan hash value: 1399315988
------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes|Cost (%CPU)| Time |
------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1| 39| 1 (0)| 00:00:01|
| 1 | TABLE ACCESS BY INDEX ROWID| TEST_NORMAL | 1| 39| 1 (0)| 00:00:01|
|* 2 | INDEX UNIQUE SCAN | IND_TEST_NORMAL| 1| | 0 (0)| 00:00:01|
------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
2 - access("EMPNO"=7369 AND "ENAME"='SMITH' AND "SAL"=800) Statistics
----------------------------------------------------------
0 recursive calls
0 db block gets
2 consistent gets
16 physical reads
0 redo size
581 bytes sent via SQL*Net to client
458 bytes received via SQL*Net from client
1 SQL*Net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
1 rows processed

3、索引范围扫描(index range scan)

索引范围扫描,不仅可以针对唯一索引,也可以针对非唯一索引。从结果集看,可以是一条记录,也可以是多条记录。

具体情况分析:

  • 对于单一列建立的索引(单一索引),当索引属于唯一索引,在检索条件中,使用该索引进行检索,且使用范围的操作符(>,<,>=,<=,between),会使用“索引范围扫描”。
  • 对于单一列建立的索引(单一索引),当索引属于非唯一索引,在检索条件中,使用该索引进行检索,且检索值不是null值,会使用“索引范围扫描”。
  • 对于多个列建立的索引(组合索引),当索引属于唯一索引,在检索条件中,使用该索引进行检索,且检索列使用组合索引涉及的部分列,但必须存在组合索引的引导列(创建组合索引时指定的第一列)时,会使用“索引范围扫描”。
  • 对于多个列建立的索引(组合索引),当索引属于非唯一索引,在检索条件中,使用该索引进行检索,检索列涉及组合索引的部分列或者全部列,但必须存在组合索引的引导列(创建组合索引时指定的第一列)时,会使用“索引范围扫描”。

需要注意:

  • 对于组合索引,当一条记录的引导列存在null值,当检索条件中,针对该条记录,仅使用该引导列进行检索时,不会使用“索引范围扫描”。
  • 对于组合索引,当一条记录的引导列存在null值,当检索条件中,针对该条记录,在使用该引导列进行检索的同时,使用组合索引的其他列且这些列针对该条记录的列值不是null值,一并进行检索,会使用“索引范围扫描”。

示例:

--创建普通索引
Yumiko@Sunny >create index ind_test_normal on test_normal(empno,ENAME,SAL);
Index created. --验证普通索引创建情况,确认为非唯一索引
Yumiko@Sunny >select INDEX_NAME,INDEX_TYPE,TABLE_NAME,UNIQUENESS from user_indexes where TABLE_NAME='TEST_NORMAL'; INDEX_NAME INDEX_TYPE TABLE_NAME UNIQUENES
------------------------- --------------------------- --------------- ---------
IND_TEST_NORMAL NORMAL TEST_NORMAL NONUNIQUE --查询记录并观察执行计划,此时扫描类型为index range scan
Yumiko@Sunny >select * from TEST_NORMAL where empno=7369 and ENAME='SMITH' and sal=800; EMPNO ENAME JOB SAL
---------- ---------- --------- ----------
7369 SMITH CLERK 800 Execution Plan
-----------------------------------------------
Plan hash value: 67814702
---------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
---------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 18 | 2 (0)| 00:00:01|
| 1 | TABLE ACCESS BY INDEX ROWID| TEST_NORMAL | 1 | 18 | 2 (0)| 00:00:01|
|* 2 | INDEX RANGE SCAN | IND_TEST_NORMAL| 1 | | 1 (0)| 00:00:01|
---------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
2 - access("EMPNO"=7369 AND "ENAME"='SMITH' AND "SAL"=800) Statistics
----------------------------------------------------------
0 recursive calls
0 db block gets
3 consistent gets
16 physical reads
0 redo size
717 bytes sent via SQL*Net to client
469 bytes received via SQL*Net from client
2 SQL*Net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
1 rows processed

4、索引跳跃扫描(index skip scan)

该索引扫描方式主要发生在组合索引上,且组合索引的引导列未被指定在检索条件中的情况下发生。
在组合索引中,无论该索引是否为唯一索引。当引导列未被指定在检索条件的情况下,可能会发生“索引跳跃扫描”

以下黄色字体内容摘引自http://book.51cto.com/art/201312/422441.htm

对于组合索引,在无前导列的情况下还能使用索引,是因为Oracle帮我们对该索引的前导列的所有distinct值做了遍历。
所谓的对目标索引的所有distinct值做遍历,其实际含义相当于对原目标SQL做等价改写(即把要用的目标索引的所有前导列的distinct值都加进来)。

例如:create index idx_employee on employee(gender,employee_id),表数据量10000行,其中gender列只有“M”跟“F”值。
当执行select * from employee where employee_id = 100时,相当于执行了等价改写,改写为:
select * from employee where gender = 'F' and employee_id = 100
union all
select * from employee where gender = 'M' and employee_id = 100;

因此,Oracle中的索引跳跃式扫描仅仅适用于那些目标索引前导列的distinct值数量较少、后续非前导列的可选择性又非常好的情形,因为索引跳跃式扫描的执行效率一定会随着目标索引前导列的distinct值数量的递增而递减。否则将执行全表扫描。

示例:

--查询测试表的总数据量
--该表总数据为22928行
Yumiko@Sunny >select count(*) from test; COUNT(*)
----------
22928 --查询测试表中owner列的列值数据分布情况
--从查询结果看,对于owner列,在22928行的数据中,只分布了3个不同的列值,数据数值的分布情况较为集中
Yumiko@Sunny >select count(distinct owner) from test; COUNT(DISTINCTOWNER)
--------------------
3 --查询测试表中object_id列的数值分布情况
--从查询结果看,对于object_id列,在22928行的数据中,分布了22912个不同的数值,该列整体的数值分布情况较为零散
Yumiko@Sunny >select count(distinct object_id) from test; COUNT(DISTINCTOBJECT_ID)
------------------------
22912 Yumiko@Sunny >desc test
Name Null? Type
----------------------------------------------------------------------
OWNER VARCHAR2(30)
OBJECT_NAME VARCHAR2(128)
SUBOBJECT_NAME VARCHAR2(30)
OBJECT_ID NUMBER
DATA_OBJECT_ID NUMBER
OBJECT_TYPE VARCHAR2(19)
CREATED DATE
LAST_DDL_TIME DATE
TIMESTAMP VARCHAR2(19)
STATUS VARCHAR2(7)
TEMPORARY VARCHAR2(1)
GENERATED VARCHAR2(1)
SECONDARY VARCHAR2(1) --以数值较为集中的owner作为组合索引的引导列,创建普通索引
Yumiko@Sunny >create index test on test(owner,OBJECT_ID);
Index created. Yumiko@Sunny >select INDEX_NAME,INDEX_TYPE,TABLE_NAME,UNIQUENESS from user_indexes where TABLE_NAME='TEST'; INDEX_NAME INDEX_TYPE TABLE_NAME UNIQUENES
------------------------- ------------------- --------------- ---------
TEST NORMAL TEST NONUNIQUE --收集测试表最新的统计信息
Yumiko@Sunny >analyze table test compute statistics for table for all columns for all indexes;
Table analyzed. --清空buffer cache缓冲池,保证无测试表的数据块存在在内存中,防止影响到测试结果
Yumiko@Sunny >alter system flush buffer_cache;
System altered. --使用之前创建的组合索引,以组合索引非引导作为条件查询列进行条件查询
--从执行计划看,oracle用到了索引扫描,而且采用index skip scan的方式进行扫描
Yumiko@Sunny >select * from test where object_id=3; Execution Plan
----------------------------------------------------------
Plan hash value: 2389257771
----------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes| Cost (%CPU)| Time |
----------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 86| 5 (0)| 00:00:01|
| 1 | TABLE ACCESS BY INDEX ROWID| TEST | 1 | 86| 5 (0)| 00:00:01|
|* 2 | INDEX SKIP SCAN | TEST | 1 | | 4 (0)| 00:00:01|
----------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
2 - access("OBJECT_ID"=3)
filter("OBJECT_ID"=3) Statistics
----------------------------------------------------------
0 recursive calls
0 db block gets
6 consistent gets
24 physical reads
0 redo size
1402 bytes sent via SQL*Net to client
469 bytes received via SQL*Net from client
2 SQL*Net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
1 rows processed --删除之前的索引
Yumiko@Sunny >drop index test;
Index dropped. --以数值分布较为零散的object_id列作为引导列,再次创建普通索引
Yumiko@Sunny >create index test on test(OBJECT_ID,owner);
Index created. --收集测试表最新的统计信息
Yumiko@Sunny >analyze table test compute statistics for table for all columns for all indexes;
Table analyzed. --清空buffer cache缓冲池,避免影响数据测试
Yumiko@Sunny >alter system flush buffer_cache;
System altered. --使用上面刚刚创建的组合索引的非引导列owner作为条件查询列进行查询操作
--从执行计划看,此次oracle未选择走索引扫描,而是采用了全表扫描的方式
--到此,印证了之前对于index skip scan方式选择的说法
Yumiko@Sunny >select * from test where owner='BI';
8 rows selected. Execution Plan
----------------------------------------------------------
Plan hash value: 1357081020
------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes| Cost (%CPU)| Time |
------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 8 | 688| 74 (2)| 00:00:01|
|* 1 | TABLE ACCESS FULL| TEST | 8 | 688| 74 (2)| 00:00:01|
------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
1 - filter("OWNER"='BI') Statistics
----------------------------------------------------------
1 recursive calls
0 db block gets
318 consistent gets
315 physical reads
0 redo size
1583 bytes sent via SQL*Net to client
469 bytes received via SQL*Net from client
2 SQL*Net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
8 rows processed

5、索引全扫描(index full scan)

对于索引全扫描,就是使用目标索引进行索引扫描时,会扫描所有索引叶块的所有索引行。

对于索引全扫描,只适用于CBO。

对于索引全扫描,使用单块读取的方式,有序读取索引块。

对于索引全扫描,从结果集看,结果全部源于索引块,而且由于已经按照索引键值顺序排序,因此不需要单独排序

对于索引全扫描,会话会产生db file sequential reads事件。

具体情况分析:

  • 对于单一列建立的索引(单一索引),当该索引列有非空约束时,在具体检索中只检索该列全部数据,会使用“索引全扫描”。
  • 对于单一列建立的索引(单一索引),当该索引列无非空约束时,在具体检索中只检索该列全部数据,且是对该列的统计(count)或者非空条件查询(is not null),会使用“索引全扫描”。
  • 对于单一列建立的索引(单一索引),当该索引列无非空约束时,在具体检索中只检索该列全部数据,且是对该列的常规查询,不会使用“索引全扫描”。(这是因为对于oracle索引,对于列中存在的null值不记录在b-tree索引中)
  • 对于多个列建立的索引(组合索引),当该索引列有非空约束时,在具体检索中只检索组合索引中涉及的全部列或者部分列的全部数据,会使用“索引全扫描”。
  • 对于多个列建立的索引(组合索引),当该索引列无非空约束时,在具体检索中只检索组合索引中涉及的全部列或者部分列的全部数据,且是对这些相关列的统计(count)或者非空条件查询(is not null),会使用“索引全扫描”。
  • 对于多个列建立的索引(组合索引),当该索引列无非空约束时,在具体检索中只检索组合索引中涉及的全部列或者部分列的全部数据,且是对该列的常规查询,不会使用“索引全扫描”。

示例:

--为测试表TEST_NORMAL的empno列添加非空约束
Yumiko@Sunny >alter table TEST_NORMAL modify(empno not null);
Table altered. --以empno列以及ename列作为组合,创建普通索引
Yumiko@Sunny >create index TEST_NORMAL_ind on TEST_NORMAL(empno,ename);
Index created. --查询上面组合索引涉及的ename列,该列不存在非空约束
--从执行计划看,oracle选择了index full scan的索引扫描方式,且结果集仅仅来源于索引块(没有根据rowid返回数据集的记录,说明无访问数据块)
Yumiko@Sunny >select ename from TEST_NORMAL;
14 rows selected. Execution Plan
----------------------------------------------------------
Plan hash value: 2425626010
------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 14 | 70 | 1 (0)| 00:00:01 |
| 1 | INDEX FULL SCAN | TEST_NORMAL_IND | 14 | 70 | 1 (0)| 00:00:01 |
------------------------------------------------------------------------------------
Statistics
----------------------------------------------------------
0 recursive calls
0 db block gets
2 consistent gets
8 physical reads
0 redo size
705 bytes sent via SQL*Net to client
469 bytes received via SQL*Net from client
2 SQL*Net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
14 rows processed

6、索引快速扫描(index fast full scan)

对于索引快速扫描,就是使用目标索引进行索引扫描时,会扫描所有索引叶块的所有索引行。

对于索引快速扫描,只适用于CBO。

对于索引快速扫描,使用多块读取的方式,读取索引块。(这种方式相较于索引全扫描,获取数据的效率更高)

对于索引快速扫描,从结果集看,结果全部源于索引块,但数据结果不一定有序。

对于索引快速扫描,会话会产生db file scattered reads事件。

具体情况分析:

参阅上面的索引全扫描,两者所谓的同宗,只是不同的需求(对于结果的响应速度或是数据序列,当然CBO优化器也会进行内部的计算评估选取最优执行路径),产生的不同的执行结果,下面的示例会展示

示例:

--查询测试表的数据量,显示此表有22928行数据
Yumiko@Sunny >select count(*) from test; COUNT(*)
----------
22928 --确认数据列中不含非空约束
Yumiko@Sunny >desc test
Name Null? Type
--------------------------------------------------------------------------------
OWNER VARCHAR2(30)
OBJECT_NAME VARCHAR2(128)
SUBOBJECT_NAME VARCHAR2(30)
OBJECT_ID NUMBER
DATA_OBJECT_ID NUMBER
OBJECT_TYPE VARCHAR2(19)
CREATED DATE
LAST_DDL_TIME DATE
TIMESTAMP VARCHAR2(19)
STATUS VARCHAR2(7)
TEMPORARY VARCHAR2(1)
GENERATED VARCHAR2(1)
SECONDARY VARCHAR2(1) --针对object_id列,创建普通索引
Yumiko@Sunny >create index test on test(object_id);
Index created. --确认上面创建的索引为非唯一索引
Yumiko@Sunny >select INDEX_NAME,INDEX_TYPE,TABLE_NAME,UNIQUENESS from user_indexes where TABLE_NAME='TEST'; INDEX_NAME INDEX_TYPE TABLE_NAME UNIQUENES
-------------------------------------------------------------------------
TEST NORMAL TEST NONUNIQUE --清空buffer_cache缓冲池数据,防止造成测试的影响
Yumiko@Sunny >alter system flush buffer_cache;
System altered. --清空shared pool缓冲池数据,防止对测试造成影响
Yumiko@Sunny >alter system flush shared_pool;
System altered. --收集测试表最新的统计信息
Yumiko@Sunny >analyze table test compute statistics for table for all columns for all indexes;
Table analyzed. --打开会话追踪,仅查看执行计划
Yumiko@Sunny >set autotrace trace --查询索引列object,同时使用is not null作为查询条件。
--由于b-tree索引中不记录null值信息,而且该列不存在非空约束,通过not null条件指定,让oracle不必考虑null值。
--从执行计划看,由于读取的数据量很大且不用排序,oracle选择了更快的多块读方式的index fast full scan,尽快获取数据。
--同样的,此时未出现access by index rowid的情况,说明未查询数据块,仅仅查询了索引块。
Yumiko@Sunny >select object_id from test where object_id is not null;
22928 rows selected. Execution Plan
----------------------------------------------------------
Plan hash value: 1645531115
-----------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
-----------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 23290 | 295K| 14 (0)| 00:00:01 |
|* 1 | INDEX FAST FULL SCAN| TEST | 23290 | 295K| 14 (0)| 00:00:01 |
----------------------------------------------------------------------------- Predicate Information (identified by operation id):
---------------------------------------------------
1 - filter("OBJECT_ID" IS NOT NULL) Statistics
----------------------------------------------------------
0 recursive calls
0 db block gets
1582 consistent gets
53 physical reads
0 redo size
502642 bytes sent via SQL*Net to client
17277 bytes received via SQL*Net from client
1530 SQL*Net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
22928 rows processed --修改上面的查询,增加排序操作。
--从执行计划看,由于此次增加了排序操作,oracle选择了偏向有序读取的index full scan的方式进行扫描。
--同样的,此次未查询数据块(不存在access by index rowid)。
Yumiko@Sunny >select object_id from test where object_id is not null order by object_id;
22928 rows selected. Execution Plan
----------------------------------------------------------
Plan hash value: 3883652822
-------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
-------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 23290 | 295K| 60 (2)| 00:00:01 |
|* 1 | INDEX FULL SCAN | TEST | 23290 | 295K| 60 (2)| 00:00:01 |
-------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
1 - filter("OBJECT_ID" IS NOT NULL) Statistics
----------------------------------------------------------
0 recursive calls
0 db block gets
1578 consistent gets
64 physical reads
0 redo size
502642 bytes sent via SQL*Net to client
17277 bytes received via SQL*Net from client
1530 SQL*Net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
22928 rows processed --再次修改查询,取消not null条件查询以及排序操作。
--此时,对于没有非空约束的object_id列,增加了null值的可能性,而b-tree索引不保存null信息。
--此次,oracle选择了全表扫描的方式。
Yumiko@Sunny >select object_id from test;
22928 rows selected. Execution Plan
----------------------------------------------------------
Plan hash value: 1357081020
--------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
--------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 22928 | 91712 | 74 (2)| 00:00:01 |
| 1 | TABLE ACCESS FULL| TEST | 22928 | 91712 | 74 (2)| 00:00:01 |
--------------------------------------------------------------------------
Statistics
----------------------------------------------------------
1 recursive calls
0 db block gets
1830 consistent gets
315 physical reads
0 redo size
415626 bytes sent via SQL*Net to client
17277 bytes received via SQL*Net from client
1530 SQL*Net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
22928 rows processed

由于篇幅有限,关于索引扫描类型的总结到此为止。若存在内容上的遗漏,或者错误,欢迎各位指教。