数据库高效优化:架构、规范与SQL技巧
上QQ阅读APP看书,第一时间看更新

7.3 索引

索引可以说是Oracle数据库中除了表以外最重要的对象了。通过添加索引来提高查询性能,也是最为常见的一种优化手段。甚至很多非DBA人员认为,数据库优化就是加索引。这种观点虽然有些偏颇,但也说明了索引对于优化的重要意义。

Oracle数据库支持多种索引。下面针对几种常用的索引分别加以介绍。

1.B树索引

B树索引是Oracle数据库的默认索引,也是最为常见的一种索引。通常我们所说的索引都是特指B树索引(见图7-1)。那为什么使用B树索引可以调高访问速度呢?这就要从索引结构来说明了。

图7-1 B树索引

整个索引结构就是一个平衡树(Balance Tree),这也就是称为B树索引的原因。在整个树结构中,包含有3种节点,分别是根节点(Root)、分支节点(Branch)、叶子节点(Leaf)。有的简单索引只有根节点和叶子节点。在根节点或分支节点中,存在一组键值范围,当通过条件访问到这些节点时,根据键值范围路由到不同的分支节点或叶子节点。例如上面示例中,如果输入的条件是'AA',那么首先查询根节点,在这个节点中有一组键值BC,它代表将键值范围分为3个区间,分别是X<B、B<X<C、C<X。因为输入的条件是'AA',故属于第一个区间,相关数据会在对应的第一个分支节点上。在第一个分支节点(L1-1)应用同样的方法,可知数据在第一个叶子节点(L0-1)。对于叶子节点来说,保存的每组记录中,每条记录包含两部分信息:一是索引键值,二是对应的行地址(ROWID)。通过行地址,就可以很快定位到数据块中的记录了。

下面通过一个示例说明为什么通过索引访问会很快。


SQL> create table t1 as select * from dba_objects;
//表已创建

SQL> insert into t1 select * from t1;
//已创建 18870 行

SQL> /
//已创建 37740 行

SQL> /
//已创建 75480 行

SQL> /
//已创建 150960 行

SQL> /
//已创建 301920 行

SQL> commit;
//提交完成

SQL> select * from t1 where object_id=20;
--------------------------------------------------------------------------
| Id  | Operation         | Name | Rows  | Bytes | Cost (%CPU)| Time     |
--------------------------------------------------------------------------
|   0 | SELECT STATEMENT  |      |    89 | 18423 |  2194   (1)| 00:00:27 |
|*  1 |  TABLE ACCESS FULL| T1   |    89 | 18423 |  2194   (1)| 00:00:27 |
--------------------------------------------------------------------------
统计信息
----------------------------------------------------------
0  recursive calls
0  db block gets
11251  consistent gets
0  physical reads

SQL> create index idx_object_id on t1(object_id);
//索引已创建

SQL> select * from t1 where object_id=20;
--------------------------------------------------------------------------------
| Id  | Operation      | Name          | Rows  | Bytes | Cost (%CPU)| Time     |
--------------------------------------------------------------------------------
|   0 | SELECT STATEMENT
                       |               |    89 | 18423 |  2188   (1)| 00:00:27 
|   1 |  TABLE ACCESS BY INDEX ROWID
                       | T1            |    89 | 18423 |  2188   (1)| 00:00:27
|*  2 |   INDEX RANGE SCAN
                       | IDX_OBJECT_ID |  2580 |       |     3   (0)| 00:00:01
--------------------------------------------------------------------------------
统计信息
----------------------------------------------------------
0  recursive calls
0  db block gets
38  consistent gets
0 physical reads
/*
从前后执行对比来看,前者走了全表扫描,后者走了索引扫描。直观地对比统计信息的“consistent gets”一栏,前者需要1万多次一致性读,后者只要数十次一致性读,两者差异巨大。自然,执行时间也差异巨大
*/

2.位图索引

位图索引是另外一种较为常见的索引,虽然说是较为常见,但也仅限于个别场景,主要适用于分析型数据库中。其原理与B树索引完全不同。在Oracle的优化器中,个别场景下可以将两类索引相互转换。这个在后面的章节会有详细说明。

下面首先来看看位图索引的结构,示例如表7-1所示。

表7-1 位图索引结构

在上面的显示中,10.0.3=>文件号+块号+行号。从表7-1可见,位图索引是在指定的地址范围,若对应记录是某个键值,则对应值设置为1,否则设置为0。从上面结构可见,如果位图索引的不同值很少,则空间占用很少。换句话说,其存储密度很高。

下面通过一个示例说明位图索引的用法。


create table t1 as select * from dba_objects where rownum<=50000;
//表已创建

update t1 set status='NOVALID' where object_id=20;
//更新3条

update t1 set status=NULL where object_id=21;
//更新1条

commit;
//提交完成
alter table t1 add constraint pk_t1 primary key(object_id);
//索引已创建

create bitmap index idx_status on t1(status);
//索引已创建

select count(*) from t1;
--------------------------------------------------------------------------------
| Id  | Operation                 | Name       | Rows  | Cost (%CPU)| Time     |
--------------------------------------------------------------------------------
|   0 | SELECT STATEMENT          |            |     1 |     2   (0)| 00:00:01 |
|   1 |  SORT AGGREGATE           |            |     1 |            |          |
|   2 |   BITMAP CONVERSION COUNT |            | 50000 |     2   (0)| 00:00:01 |
|   3 |    BITMAP INDEX FAST FULL SCAN
                                  | IDX_STATUS |       |            |          |
--------------------------------------------------------------------------------
Statistics
----------------------------------------------------------
5  consistent gets
/*
默认使用位图索引,并且走的是位图索引快速全扫描。即使位图索引字段有空值,由于位图索引保存空值,因此也没有问题。此外,这也要看位图索引字段值的基数,如果基数较低,则该位图索引较小;如果基数很大,则位图索引会很大。在基数很大的情况下,COUNT(*)会选择B树索引,而不会走位图索引扫描
*/

select /*+ index(t1 pk_t1) */ count(*) from t1;
------------------------------------------------------------------
| Id  | Operation        | Name  | Rows  | Cost (%CPU)| Time     |
------------------------------------------------------------------
|   0 | SELECT STATEMENT |       |     1 |   106   (1)| 00:00:02 |
|   1 |  SORT AGGREGATE  |       |     1 |            |          |
|   2 |   INDEX FULL SCAN| PK_T1 | 50000 |   106   (1)| 00:00:02 |
------------------------------------------------------------------
Statistics
----------------------------------------------------------
105  consistent gets
//强制使用主键索引(B树索引),可看到一致性读大大增加。这也间接说明了位图索引的高密度存储特点

3.其他索引

上面我们谈到了最为常见的两种索引类型,下面再看看其他索引类型。从本质上来讲,它们还是B树索引或者位图索引。

(1)函数索引

函数索引就是将一个函数计算的结果存储在列中,而不是存储列数据本身,可以把基于函数的索引看成是一个虚拟列上的索引。总之,所谓函数索引也只不过是基于已加工的逻辑列所创建的索引而已。


SQL> create table t1 as select * from dba_objects;
//表已创建

SQL> create index idx_object_name on t1(object_name);
//索引已创建

SQL> select * from t1 where upper(object_name)='EMP';
--------------------------------------------------------------------------
| Id  | Operation         | Name | Rows  | Bytes | Cost (%CPU)| Time     |
--------------------------------------------------------------------------
|   0 | SELECT STATEMENT  |      |     3 |   621 |    74   (0)| 00:00:01 |
|*  1 |  TABLE ACCESS FULL| T1   |     3 |   621 |    74   (0)| 00:00:01 |
--------------------------------------------------------------------------
//虽然在object_name字段上建立了索引,但是由于使用了upper()函数,导致无法利用该索引

SQL> create index idx_object_name_upper on t1(upper(object_name));
//索引已创建

SQL> select * from t1 where upper(object_name)='EMP';
---------------------------------------------------------------------------------
| Id  | Operation          | Name                  | Rows  | Bytes | Cost (%CPU)| 
---------------------------------------------------------------------------------
|   0 | SELECT STATEMENT   |                       |   179 | 48867 |    35   (0)| 
|   1 |  TABLE ACCESS BY INDEX ROWID
                           | T1                    |   179 | 48867 |    35   (0)| 
|*  2 |   INDEX RANGE SCAN | IDX_OBJECT_NAME_UPPER |    72 |       |     1   (0)| 
---------------------------------------------------------------------------------
//创建了单独的函数索引,此时的查询就可以利用索引

(2)虚拟列索引

这里要先谈一下虚拟列。虚拟列是在11g中新引入的一个技术,从字面上看,创建的列不是真正的物理保存,只是一个定义。而基于虚拟列创建的索引,就是虚拟列索引。在某种程度上,虚拟列索引和上面谈到的函数索引有些类似。

(3)虚拟索引

在11g中,Oracle可以通过NOSEGMENT子句命令创建一个永远不会使用且不会为其分配任何盘区的索引。如果想要创建一个很大的索引,但并不想给它分配空间,则要先确定优化器是否会选择使用该索引。如果确定了这个索引是有用的,可以删除该索引,然后使用不包含NOSEGMENT的语句重建它。

(4)不可见索引

不可见索引不是一种特殊的索引类型,而是使索引对优化器“不可见”,导致没有查询会使用它。这对于评估索引使用效果非常有帮助,特别是对某些第三方应用,无法修改代码,这个特性十分有用。下面通过一个示例说明。


SQL> create table t1 as select * from dba_objects;
//表已创建

SQL> create index idx_id on t1(object_id);
//索引已创建

SQL> select * from t1 where object_id=20;
--------------------------------------------------------------------------------
| Id  | Operation             | Name   | Rows  | Bytes | Cost (%CPU)| Time     |
--------------------------------------------------------------------------------
|   0 | SELECT STATEMENT      |        |     3 |   621 |     3   (0)| 00:00:01 |
|   1 |  TABLE ACCESS BY INDEX ROWID
                              | T1     |     3 |   621 |     3   (0)| 00:00:01 |
|*  2 |   INDEX RANGE SCAN    | IDX_ID |    73 |       |     1   (0)| 00:00:01 |
--------------------------------------------------------------------------------

SQL> alter index idx_id invisible;
//索引已更改

SQL> select * from t1 where object_id=20;
--------------------------------------------------------------------------
| Id  | Operation         | Name | Rows  | Bytes | Cost (%CPU)| Time     |
--------------------------------------------------------------------------
|   0 | SELECT STATEMENT  |      |     3 |   621 |    74   (0)| 00:00:01 |
|*  1 |  TABLE ACCESS FULL| T1   |     3 |   621 |    74   (0)| 00:00:01 |
--------------------------------------------------------------------------
//将索引设置为不可见后,优化器将不考虑这个索引,因此选用了全表扫描方式

(5)压缩索引

Oracle中的索引键允许压缩存储索引键中前面重复的部分,并且是每个叶块而不是每个叶块中的每行存储重复的值。压缩和非压缩索引在使用上差别不大,但压缩索引能节省大量空间。利用压缩索引,块缓冲区缓存比以前能存放更多的索引条目,缓存命中率可能会上升,物理I/O应该会下降,但是要多占用一些CPU时间来处理索引,还会增加块竞争的可能性。

下面通过一个示例说明。


SQL> create table t as select * from all_objects;
//表已创建

SQL> create table idx_stats as select '       ' what,a.* from index_stats a where 1=0;
//表已创建

SQL> create index t_idx_0 on t(owner,object_type,object_name);
//索引已创建

SQL> analyze index t_idx_0 validate structure;
//索引已分析

SQL> insert into idx_stats select 'compress_0',a.* from index_stats a where a.name='T_IDX_0';
已创建 1 行

SQL> drop index t_idx_0;
//索引已删除

SQL> create index t_idx_1 on t(owner,object_type,object_name) compress 1;
//索引已创建

SQL> analyze index t_idx_1 validate structure;
//索引已分析

SQL> insert into idx_stats select 'compress_1',a.* from index_stats a where a.name='T_IDX_1';
//已创建 1 行

SQL> drop index t_idx_1;
//索引已删除

SQL> create index t_idx_2 on t(owner,object_type,object_name) compress 2;
//索引已创建

SQL> analyze index t_idx_2 validate structure;
//索引已分析

SQL> insert into idx_stats select 'compress_2',a.* from index_stats a where a.name='T_IDX_2';
//已创建 1 行

SQL> select what,height,lf_blks,br_blks,btree_space,opt_cmpr_count,opt_cmpr_pctsave from idx_stats;
WHAT        HEIGHT  LF_BLKS  BR_BLKS BTREE_SPACE OPT_CMPR_COUNT OPT_CMPR_PCTSAVE
---------- ------- -------- -------- ----------- -------------- ----------------
compress_0       2      109        1      880032              2               29
compress_1       2       94        1      759656              2               18
compress_2       2       76        1      615728              2                0
/*
从输出可见,对于不压缩、压缩一个字段(compress=1)、压缩两个字段(compress=2),对应索引的叶子节点明显减少
*/

(6)复合索引

当某个索引包含多个已索引列时,这个索引就称为复合索引。如果查询条件中包含多个列,往往可以应用到复合索引。下面通过一个示例说明。


SQL> create table t1 as select * from dba_objects;
//表已创建

SQL> create index idx_1 on t1(owner,object_id);
//索引已创建

SQL> select * from t1 where owner='SYS' and object_id=20;
--------------------------------------------------------------------------------
| Id  | Operation              | Name  | Rows  | Bytes | Cost (%CPU)| Time     |
--------------------------------------------------------------------------------
|   0 | SELECT STATEMENT       |       |     2 |   414 |     2   (0)| 00:00:01 |
|   1 |  TABLE ACCESS BY INDEX ROWID
                               | T1    |     2 |   414 |     2   (0)| 00:00:01 |
|*  2 |   INDEX RANGE SCAN     | IDX_1 |     1 |       |     1   (0)| 00:00:01 |
--------------------------------------------------------------------------------
//此时利用了复合索引

(7)反转索引

反转索引是一种特殊的B树索引。它将索引列中列值的每个字节的位置反转。例如“12345”,反转之后是“54321”。其最大特点就是对于原来相连比较紧密的值,强制使其分散到相距比较远的位置上。这样可以使数据更均匀地分布。但由于反转索引的特点,导致只有精准匹配查找才能使用反转索引。下面通过一个示例说明。


create table t1 as select rownum id from dba_objects;
create index t1_idx on t1(id);
alter index idx_ t1_idx rebuild reverse;