11 Indexes
本章提要
--------------------------------------
索引会影响 DML 与 select 操作, 要找到平衡点
最好从一开始就创建好索引
索引概述
B*索引
其他一些索引
索引使用中的一些基本问题
--------------------------------------
索引概述
oracle提供的索引种类:
B*树索引, 我们所说的"传统"索引, 并不是二叉树, 这里的"B" 代表平衡, B*树索引的子类型:
索引组织表
B*树聚簇索引
降序索引, 允许索引结构中按"从大到小"顺序排序
反向键索引, 键中的字节会"反转"
位图索引:
在B*树中, 通常索引条目和行之间存在一种一对一的关系, 一个索引条目就指向一行, 而对于位图索引, 一个索引条目则使用一个位图
同时指向多行, 位图索引适用于高度重复而且通常只读的数据, 在一个OLTP数据库中, 由于存在并发性相关的问题, 所以不能考虑使用
位图索引:
位图联结索引, 在多表联结时, 可能被使用, 但是, 同样, OLTP中不能使用.
基于函数的索引:
这些就是 B* 树索引或位图索引, 它将一个函数计算得到的结果存储在行的列中, 而不是存储列数据本身, 可以把基于函数的索引看做
一个虚拟列(或派生列)上的索引, 换句话说, 这个列并不物理地存储在表中, 基于函数的索引可以用于加快形如 select * from t
where function(database_column) = some_value这样的查询, 因为值 function(database_column)已经提前计算并存储在索引中.
应用域索引:
B* 树索引
我们所说的"传统"索引, 数据库最常用的一类索引结构, 其实现与二叉查找树很相似, 如图:
B*树索引不存在非唯一条目, (索引也是一个存储结构, 在这个存储结构中不存在两行完全相同的数据), 在一个非唯一的索引中, oracle会
把rowid作为一个额外的列追加到键上, 使得键唯一, 例如 create index i on t(x,y)索引, 从概念上讲, 它就是create unique index i
on t(x, y, rowid). 在一个唯一索引中, 根据你定义的唯一性, oracle 不会再向索引键增加rowid.
B*树特点之一是, 所有叶子块都应该在树的同一个层上, 这一层也称为索引的高度, 这说明所有索引的根块到叶子块的遍历都会访问同样
数目的块, 大多数B*树索引的高度都是2或3, 即使索引中有数百万行记录也是如此, 这说明, 一般来讲, 在索引中找到一个键值只需要执行
2或3次I/O. (blevel统计的是分支数, 即比高度少1)
select index_name, blevel, num_rows from user_indexes where table_name = 'BIG_TABLE';
B*树是一个绝佳的通用索引机制, 无论是大表还是小表都适用, 随着底层表大小的增长, 获取数据的性能只稍有恶化(或根本不恶化)
索引键压缩(个人感觉用处不大)
索引键条目分解为两部分, 前缀和后缀, 前缀有重复, 后缀没有重复(唯一区域)
实验,
create table t as select * from all_objects where rownum <= 50000; create index t_idx on t(owner,object_type,object_name); analyze index t_idx validate structure; create table idx_stats as select 'noncompressed' what, a.* from index_stats a; drop index t_idx; create index t_idx on t(owner,object_type,object_name) compress &1; -- 分别用1,2,3来代替 analyze index t_idx validate structure; insert into idx_stats select 'compress &1', a.* -- 分别用1,2,3来代替 from index_stats a; select what, height, lf_blks, br_blks, btree_space, opt_cmpr_count, opt_cmpr_pctsave from idx_stats /
对现在来说, 这种压缩并不是免费的, 现在压缩索引比原来更复杂了, oracle会花更多时间来处理这个索引结构中的数据, 不光在修改期间
维护索引更耗时, 查询期间搜索索引也更花时间.
反向键索引(还比较重要, 分情况使用)
反向键索引只是将索引键中各个列的字节反转, 如果考虑 90101, 90102, 和90103, 如果使用 oracle dump函数查看其内部表示, 可以看到
这几个数表示如下:
select 90101, dump(90101,16) from dual
union all
select 90102, dump(90102,16) from dual
union all
select 90103, dump(90103,16) from dual
/
结果是:
90101 DUMP(90101,16)
---------- ---------------------
90101 Typ=2 Len=4: c3,a,2,2
90102 Typ=2 Len=4: c3,a,2,3
90103 Typ=2 Len=4: c3,a,2,4
3 rows selected.
每个数的长度都是4字节, 它们只是最后一个字节有所不同, 这些书最后可能在一个索引结构中向右一次放置(放置的比较近), 不过, 如果
反转这些数的字节, oracle就会插入以下值:
90101 reversed = 2,2,a,c3
90102 reversed = 3,2,a,c3
90103 reversed = 4,2,a,c3
这些数之间最后可能相距很远, 这样访问同一个块(最右边的块)的RAC实例个数就能减少, 反向键索引的缺点之一是, 能用常规索引的地方
不一定能用反向键索引, 例如: 在回答谓词时, x 上的反向键索引就没用: where x > 5
存储之前, 数据部是按 x 在索引中派讯, 而是按 reverse(x)排序, 因此, 对 x>5的区间扫描不能使用这个索引. 另外有些谓词有可以,比如
在(x,y)上有一个串联索引, where x = 5 就可以, 这是因为, 首先将x的字节翻转, 然后再将 y 的字节翻转, oracle并不是将(x||y)的字节
反转, 而是会存储(reverse(x) || reverse(y)), 这说明, x = 5 的所有值会存储在一起, 所以 oracle 可以对这个索引执行区间扫描来
找到所有这些数据.
下面假设一个用序列填充的表上有一个代理主键,而且不需要在这个(主键)索引上使用区间扫描, 也就是说, 不需要 max(primary_key),
where primary_key < 100 等查询(如果有这个查询条件, 那么就不能使用反向索引, 因为反向索引在这种情况下不能被使用), 在有大量插入
操作的情况下, 即使只有一个实例, 也要使用反向索引. 下面测试:
create table t tablespace assm as select 0 id, a.* from all_objects a where 1=0; alter table t add constraint t_pk primary key (id) using index (create index t_pk on t(id) &indexType tablespace assm); create sequence s cache 1000; -- 如果把 &indexType 替换为reverse, 就会创建一个反向索引, 如果不加&indexType -- 即替换为"什么也没有", 则表示使用一个"常规"索引 -- 开始测试 create or replace procedure do_sql as begin for x in ( select rownum r, all_objects.* from all_objects ) loop insert into t ( id, OWNER, OBJECT_NAME, SUBOBJECT_NAME, OBJECT_ID, DATA_OBJECT_ID, OBJECT_TYPE, CREATED, LAST_DDL_TIME, TIMESTAMP, STATUS, TEMPORARY, GENERATED, SECONDARY ) values ( s.nextval, x.OWNER, x.OBJECT_NAME, x.SUBOBJECT_NAME, x.OBJECT_ID, x.DATA_OBJECT_ID, x.OBJECT_TYPE, x.CREATED, x.LAST_DDL_TIME, x.TIMESTAMP, x.STATUS, x.TEMPORARY, x.GENERATED, x.SECONDARY ); if ( mod(x.r,100) = 0 ) then commit; end if; end loop; commit; end; / -- c 代码, 目前无法测试 exec sql declare c cursor for select * from all_objects; exec sql whenever notfound do break; for(;;) { exec sql fetch c into :owner:owner_i, :object_name:object_name_i, :subobject_name:subobject_name_i, :object_id:object_id_i, :data_object_id:data_object_id_i, :object_type:object_type_i, :created:created_i, :last_ddl_time:last_ddl_time_i, :timestamp:timestamp_i, :status:status_i, :temporary:temporary_i, :generated:generated_i, :secondary:secondary_i; exec sql insert into t ( id, OWNER, OBJECT_NAME, SUBOBJECT_NAME, OBJECT_ID, DATA_OBJECT_ID, OBJECT_TYPE, CREATED, LAST_DDL_TIME, TIMESTAMP, STATUS, TEMPORARY, GENERATED, SECONDARY ) values ( s.nextval, :owner:owner_i, :object_name:object_name_i, :subobject_name:subobject_name_i, :object_id:object_id_i, :data_object_id:data_object_id_i, :object_type:object_type_i, :created:created_i, :last_ddl_time:last_ddl_time_i, :timestamp:timestamp_i, :status:status_i, :temporary:temporary_i, :generated:generated_i, :secondary:secondary_i ); if ( (++cnt%100) == 0 ) { exec sql commit; } } exec sql whenever notfound continue; exec sql commit; exec sql close c;
测试的结果是(书上看的,目前无法测试)使用反向索引高效的多
降序索引(个人感觉, 作用一般)
测试:
create table t as select * from all_objects / create index t_idx on t(owner,object_type,object_name); begin dbms_stats.gather_table_stats ( user, 'T', method_opt=>'for all indexed columns' ); end; / set autotrace traceonly explain select owner, object_type from t where owner between 'T' and 'Z' and object_type is not null order by owner DESC, object_type DESC; -- oracle 会往前读索引 -- 这个explain 计划中最后没有排序步骤, 数据已经是有序的, 不过, 如果你有一组列, -- 其中一些列按升序排序(ASC), 另外一些列按降序排列(DESC), 此时降序索引就能用.例如: select owner, object_type from t where owner between 'T' and 'Z' and object_type is not null order by owner DESC, object_type ASC; -- oracle 不能再使用(owner, object_type, object_name)上索引对数据排序, 它可以往前 -- 读得到按 owner desc 排序的数据, 但是现在还需要"向后读"来得到按object_type升序 -- ASC的数据, 此时oracle的实际做法是, 它会把所有行收集起来,然后排序, 但是如果有 -- DESC索引, 则有: create index desc_t_idx on t(owner desc,object_type asc); select owner, object_type from t where owner between 'T' and 'Z' and object_type is not null order by owner DESC, object_type ASC;
查询中最好别少了 orader by, 即使你的查询计划中包含一个索引, 但这并不表示数据会以"某种顺序"返回, 要想从数据库以某种有序的顺序
获取数据, 唯一的办法就是在查询中包括一个 order by 子句, order by 是无可替代的.
什么情况下应该使用 B* 树索引
仅当要通过索引访问表中很少的一部分(只占很小百分比, 2%), 才能使用 B* 树在裂伤建立索引
如果要处理表中的多行, 而且可以使用索引而不用表, 就可以使用一个B*树索引.(一个查询, 索引包含了足够的信息来回答查询, 我们根本
不用去访问表) select count(*) from t 就是只读索引就可以了, 不需要访问表了.
重要的是, 要了解这两个概念间的区别, 如果必须完成 table access by index rowid, 就必须确保只访问表中很少的一部分块(很小百分比),
如果我们访问的行太多(所占百分比过大, 20%以上), 那么与全表扫描相比, 通过B*树索引来访问这些数据通常要花更长的时间.
一般来讲, B*树索引会放在频繁使用查询谓词的裂伤, 而且我们希望从表中只返回少量的数据, 在一个thin表(也就是说很少的列, 或列很小),
这个百分比相当小(2%~3%), 如果在一个fat表(也就是说, 有很多列, 或列很宽)百分比可能会升到表的20%~25%, 索引按索引建的顺序存储,
索引会按键的有序顺序访问, 索引指向的块则随机的存储在堆中, 因此,我们通过索引访问表时, 会执行大量分散, 分散是指: 索引会告诉我们
读取块1, 然后是块1000, 块205, 块321, 等等, 它不会要求我们按一种连续的方式读取块1, 块2, 块3 等等.
下面来看一个例子:
假设我们索引读取一个thin表, 而且要读取表中20%的行, 若这个表中有100 000行, 其中20%就是20000行, 如果行大小为80字节,
在一个块大小为8kb的数据库中, 每个块上大约100行, 这说明, 大约是20000个 table access by rowid 操作, 为此要处理20000个表快来执行
查询, 不过, 整个表才只有1000个块, 在这种情况下 全表扫描就比索引高效的多.
1) 物理组织
数据在磁盘上如何物理地组织, 对上述计算会有显著影响, 因为这会大大影响索引访问的开销, 假设一个表, 其中的行主键由一个序列来
填充, 向这个表增加数据时, 序列号相邻的行一般存储位置也会彼此相邻.表会很自然的按主键顺序聚簇(因为数据或多或少就是以这种
顺序增加的), 当然, 它不一定严格按照聚簇(要想做到这一点, 必须使用一个IOT), 一般来讲, 主键值彼此接近的行的物理位置也会靠在
一起, 如果发生下面查询: select * from t where primary_key between :x and :y;
你想要的行通常就位于同样的块上, 在这种情况下, 即使要访问大量的行(占很大的百分比), 索引区间扫描可能也很有用, 原因在于:
我们需要读取和重新读取的数据库块可能会被缓存, 因为数据共同放置在同一个位置, 另一方面, 如果行并非共同存储在一个位置上, 使用
这个索引对性能来讲可能就是灾难性的. 下面演示:
create table colocated ( x int, y varchar2(80) ); begin for i in 1 .. 100000 loop insert into colocated(x,y) values (i, rpad(dbms_random.random,75,'*') ); end loop; end; / alter table colocated add constraint colocated_pk primary key(x); begin dbms_stats.gather_table_stats( user, 'COLOCATED'); end; / -- 这个表正好满足前面的描述, 即在块大小为8kb的一个数据库中, 每块大约 100 行, -- 在这个表中, x = 1,2,3 的行极有可能在同一个块上, 仍取这个表, 但有意的使他 -- "无组织", 在 colocated表中, 我们创建了一个y列, 它带有一个前导随机数, 现在 -- 利用这一点使得数据无组织, 即不再按主键排序: create table disorganized as select x,y from colocated order by y; -- 这时, 数据已经无序的存储在磁盘上了 alter table disorganized add constraint disorganized_pk primary key (x); begin dbms_stats.gather_table_stats( user, 'DISORGANIZED'); end; / -- 比较这两个表,虽然含有相同的结果集, 但是在性能上却天壤之别
2) 聚簇因子
我们查看 user_indexes视图中的 clustering_factor 列, 这个列有以下含义:
根据索引的值指示表中行的有序程度
如果这个值与块数接近, 则说明表相当有序, 得到了很好的组织, 在这种情况下, 同一个叶子块中的索引条目可能指向同一个数据
块上的行.
如果这个值与行数接近, 表的次序可能就是非常随机的, 在这种情况下, 同一个叶子块上的索引条目不太可能指向同一个数据块上的行.
可以把聚簇因子看做是通过索引读取整个表时对表执行的逻辑I/O次数. 查看索引时, 会得到以下结果:
select a.index_name,
b.num_rows,
b.blocks,
a.clustering_factor
from user_indexes a, user_tables b
where index_name in ('COLOCATED_PK', 'DISORGANIZED_PK' )
and a.table_name = b.table_name
INDEX_NAME NUM_ROWS BLOCKS CLUSTERING_FACTOR
------------------------------ ---------- ---------- -----------------
COLOCATED_PK 100000 1252 1190
DISORGANIZED_PK 100000 1219 99930
所以, 数据库说, "如果通过索引 COLOCATED_PK从头到尾读取COLOCATED表中的每一行, 就要执行1190次I/O, 不过我们队DISORGANIZED表做
同样的事情, 则会对这个表执行 99930次 I/O" 差别的原因在于: 当oracle对索引结构执行区间扫描时, 如果它发现索引中的下一行与前
一行在同一个数据库块上, 就不会再执行另一个 I/O, 不过, 如果下一行不在同一个块上, 就会释放当前的这个块, 而执行令一个I/O从
缓冲区缓存获取到处理的下一个块, 因此, 在我们对索引执行区间扫描时, COLOCATED_PK索引就会发现下一行几乎总与前一行在同一个块上,
DISORGANIZED_PK 索引发现的情况则恰好相反.
位图索引
是为数据仓库, 即查询环境设计的, 位图索引的结构: 其中用一个索引键条目存储指向多行的指针, 这与 B*树结构部同, 在位图索引中, 可能只有
很少的索引条目, create BITMAP index job_idx on emp(job);
什么情况下使用位图索引:
差异数低的数据最为合适, 比如性别, 在一个上亿条记录的表中, 100000也能算差异低的数据.
位图索引联结(个人感觉用处不大)
通常在都是在一个表上创建索引,而且值使用这个表的列, 位图联结索引则打破了这个规则, 它允许使用另外某个表的列对一个给定表建立索引, 例如:
create bitmap index emp_bm_idx
on emp( d.dname )
from emp e, dept d
where e.deptno = d.deptno
/
基于函数的索引(有点用)
利用基于函数的索引, 我们能够对计算得出的列建立索引, 并在查询中使用这些索引.
简单的基于函数索引的例子:
我们想在EMP表的ENAME列上执行一个大小写无关的搜索, 在基于函数的索引引入之前, 我们可能必须采用另外一种完全不同的方式来做, 可能要为EMP
表增加一个额外的列, 例如名为 UPPER_ENAME的列, 这个列由 insert 和 update 上的一个触发器维护, 这个触发器只是设置
NEW.UPPER_NAME := UPPER(:NEW.ENAME). 另外要在这个额外的列上建立索引, 但是, 现在我们可以利用基于函数的索引了.
create table emp as select * from scott.emp where 1=0; insert into emp (empno,ename,job,mgr,hiredate,sal,comm,deptno) select rownum empno, initcap(substr(object_name,1,10)) ename, substr(object_type,1,9) JOB, rownum MGR, created hiredate, rownum SAL, rownum COMM, (mod(rownum,4)+1)*10 DEPTNO from all_objects where rownum < 10000; create index emp_upper_idx on emp(upper(ename)); -- 接下来分析这个表, 让查询使用函数索引 -- 从 oracle 10g以后, 这步骤不必要, 因为默认就会使用 begin dbms_stats.gather_table_stats (user,'EMP',cascade=>true); end; / select * from emp where upper(ename) = 'KING'; -- 通过执行计划, 可以看到, access(upper("ENAME")='KING')
计划函数的索引除了对使用内置函数的查询显然有帮助之外(自定义的函数也可以), 还可以用来有选择地只是对表中的某些行建立索引, 如果在表T上
有一个索引 I: create index I on t(a, b); -- 这是B*树索引, 而且行中的A和B都为NULL, 索引结构中就没有相应的条目, 如果只对表中的某些行
建立索引, 这就能用的上, 考虑一个很大的表, 其中有一个 NOT NULL列, 名为 PROCESSED_FLAG, 它有两个可取值: Y或N, 默认为N, 增加新行时,
这个值都为N, 指示这一行未得到处理, 等到处理了这一行后, 则会将其更新为Y来指示已处理, 我们可能想对这个列建立索引, 从而能很快速的获取
值为N的记录, 但是这里有数百万行, 而且几乎所有的行的值都为Y, 所得到的B*树索引将会很大, 如果我们把值从N更新为Y, 维护这样一个大索引的
开销也相当高, 如果我们能只对感兴趣的记录建立索引(即该列值为 N 的记录).我们可以编写一个函数, 如果不想对某个给定的行加索引, 则这个
函数就返回NULL, 而对想加索引的行则返回一个非NULL值. 例如, 由于我们只对列值为N的记录感兴趣, 所以只对这些记录加索引:
create index processed_flag_idx on big_table( case temporary when 'N' then 'N' end ); analyze index processed_flag_idx validate structure; select name, btree_space, lf_rows, height from index_stats;
当你在 to_date 函数上创建索引, 有时候并不能成功, 要在基于函数的索引使用to_date, 必须使用一种无歧义的确定性日期格式, 比如你用YYYY,
to_date就是不确定的.
应用域索引(个人感觉没用)
oracle 所谓的扩展索引, 利用应用域索引, 你可以创建自己的索引结构, 使之像oracle提供的索引一样工作. 举例, oracle自己的文本索引
索引的常见问题
以下就是大师回答过最多关于索引的问题的一个小总结.
1) 视图能使用索引么?
视图实际上就是一个存储查询, oracle 会把查询中访问视图的有关文本代之以视图定义本身, 视图只是为了方便最终用户, 优化器还是对基表
使用查询, 使用视图时, 完全可以考虑使用为基表编写的查询中所能用的所有索引, 对视图建立索引实际上就是对基表建立索引.
2) null 和 索引能协作么?
B*树索引不会存储完全为null的条目, 而位图和聚簇索引则不同, 另外, 还有人问, 为什么我的查询不使用索引, select * from t where x
is null; 这个查询无法使用索引, 正是因为 B*树索引不会存储完全为null的条目, 但是如果你的索引键值中包含1列不为null的情况, 那么就
可以使用索引, 如下例子:
create table t ( x int, y int NOT NULL ); create unique index t_idx on t(x,y); insert into t values ( 1, 1 ); insert into t values ( NULL, 1 ); begin dbms_stats.gather_table_stats(user,'T'); end; / set autotrace on select * from t where x is null; -- 这时, 显示使用了索引
3) 外键是否应该加索引?
前面章节中已经讨论过, 外键必须加索引.前几张也讨论了什么情况可以不对外键加索引(个人建议, 强烈加索引)
4) 为什么没有使用我的索引?
情况1, 我们在使用一个B*树索引, 而且谓词中没有使用索引的最前列.
如果是这种情况, 可以假设一个表T, 在T(X,Y)上有一个索引, 我们要做查询: select * from t where y = 5, 此时, 优化器不打算使用
T(x,y)上的索引, 因为谓词中不涉及X列, 在这种情况下, 倘若使用索引, 可能就必须查询每一个索引条目(稍后会讨论一种索引跳跃式扫描,
这是一种例外情况), 而优化器通常更倾向于对T做全表扫描, 但这并不完全排除使用索引, 如果查询是 select x, y from t where y = 5,
优化器就会注意到, 它不必全面扫描表来得到X 或 y, 对索引本身做一个快速的全面扫描会更合适, 因为这个索引一般比底层表小的多. 另
一种情况下CBO也会使用T(X,Y)上的索引, 这就是索引跳跃式扫描, 当且仅当索引的最前列(在上一个例子中, 最前列就是x)只有很少的几个
不同值, 而且优化器了解这一点, 跳跃式扫描就能很好的发挥作用.
情况2, 我们使用 select count(*) from t 查询(或类似查询), 而且在表t上有一个B*树索引, 不过, 优化器并不是统计索引条目, 而是全表扫描
在这种情况下, 索引可能建立在一些允许有null值的列上, 由于对于索引键完全为null的行不会建立相应的索引条目, 所以索引中的行数可
能并不是表中的行数, 这里优化器选择是对的(当然, 如果这种情况使用索引, 那返回的行就不够了)
情况3, 对于一个有索引的列, 做以下查询: select * from t where f(indexed_column) = value, 发现没有使用索引?
原因是这个列上使用了函数, 我们是对 index_column 的值建立的索引, 而不是对 f(indexed_column)建立的索引, 因此不能使用这个索引,
如果愿意, 可以另外对函数建立索引.(这个列上即有普通索引, 又有函数索引)
情况4, 我们已经对一个字符列建立了索引, 这个列只包含数值数据, 如果使用以下语法来查询:
select * from t where indexed_column = 5 注意查询中的数字5是常数5(而不是一个字符串), 此时就没有用indexed_column上的索引.
因为, 我们队这个列隐式的应用了一个函数, select * from t where to_number(indexed_column) = 5, 这样就跟情况3一样了. 如果可能
的话, 陶若谓词中有函数, 尽量不要对数据库列应用这些函数, 比如:
where date_col >= trunc(sysdate) and date_col < trunc(sysdate+1), 可见应该尽量将函数应用在值上, 而不是列上.
情况5, 有时使用了索引, 实际上反而会更慢, 例如:
create table t ( x, y , primary key (x) ) as select rownum x, object_name from all_objects / begin dbms_stats.gather_table_stats ( user, 'T', cascade=>true ); end; / set autotrace on explain select count(y) from t where x < 50; -- 优化器使用索引 select count(y) from t where x < 15000; -- 全表扫描
对于查询调优时, 如果发现你认为本该使用的某个索引实际上并没有使用, 就不要冒然强制使用这个索引, 而应该先做个测试, 并证明使用
这个索引后确实会加快速度, 然后再考虑强制使用索引.
情况6, 有一段时间没有分析表, 这些表起先很小, 但等到查看时, 它已经增长的非常大, 有时候, 分析这个表, 然后就会使用索引. 分析表的
作用时, 将这个表的正确的统计信息反馈给 CBO, 这样CBO能作出正确的选择.