有时我们创建了字段上的索引,但是通过执行计划却发现索引并没有被使用,还是会使用全表扫描。随着表上数据量的增长,性能会越来越差。如果不能查明原因,就只能盲目的靠不断扩容硬件来缓解(不是解决)这类问题,投资巨大,收益甚微。有时实在没办法,只能通过定期清空表来解决。

对于大表来说,能否使用到索引,效率上可能会相差上万倍(表越大,性能差距就越大)。下面我们就列举有索引,但是没有被使用的10几种情况,并给出对应的解决方法:

1. 索引状态为UNUSABLE

--普通索引,分区表global index比较容易因为删除分区导致unusable

select * from dba_indexes where status='UNUSABLE';

--分区索引

select * from dba_ind_partitions where status='UNUSABLE';

--子分区索引

select * from dba_ind_subpartitions where status='UNUSABLE';

应对方法:

处于unusable状态的索引,需要rebuild ,才可能被使用。

alter index idx_name rebuild;

alter index idx_name rebuild partition part_name;

alter index idx_name rebuild subpartition subpart_name;

2. 索引状态为INVISIBLE

select table_owner,table_name,index_name,visibility from dba_indexes where visibility='INVISIBLE';

应对方法:

alter index idx_name visible;

如果把参数optimizer_use_invisible_indexes 改为true(默认是false),那么invisible状态的索引也可以正常使用,不建议。

3. 优化器估算使用索引COST高

3.1 字段的NDV(唯一值个数)小,数据平均分布,不使用索引是正确的选择

比如在正常的gender(性别)字段上创建索引,where gender='M' ,即使gender字段上有索引,也不会使用,但是可以使用hint强制使用该索引,只是效率不如不使用索引。

如果还有除了M、F外的其他性别,占的比例又比较少,比如:where gender='X' ,这种情况优化器会自动使用gender字段上的索引(字段上的直方图信息是选择使用索引的依据)。

应对方法:

正确选择,不需要干预。如果修改了optimizer_index_caching或optimizer_index_cost_adj参数,让优化器误用低效索引,性能反而会下降。

3.2 字段的NDV(唯一值个数)小,分布不均,但是没有直方图信息

上面例子,如果gender字段上没有直方图信息,即便我们知道where gender='X' 使用索引效率高,优化器也不会使用索引。因为在没有直方图信息的情况下,优化器会按照数据平均分布来计算COST,唯一值越小,COST越高。

应对方法:

可以使用hint强制使用索引。直方图信息默认会自动收集,不建议主动关闭直方图信息收集。有了直方图信息,索引会自动使用。

3.3 字段数据分布不均,有直方图信息。但是SQL使用了绑定变量,而且隐含参数_optim_peek_user_binds(绑定变量窥视)被关闭(默认是开启,有少量生产数据库关闭了这个参数)

还是上面例子,where gender=:b1 ,在b1赋值 'X' 时,我们希望SQL执行计划能使用索引,但是在绑定变量窥视关闭的情况下,优化器不会选择使用索引,理由还是cost高。

应对方法:

如果绑定变量b1经常使用的变量值是'X',这种情况可以使用hint强制使用索引。

如果绑定变量值仍有一些使用'F'、'M'的查询,这种情况不建议使用hint强制使用一种执行计划。建议开启绑定变量窥视(生产系统为了避免全局影响,可以在sql级别通过hint开启绑定变量窥视/*+ OPT_PARAM('_optim_peek_user_binds' 'true') */),并且保持ACS(自适应游标)开启,必要时还需要配合 bind_aware的hint。

3.4 like '%abcde%'

select ... from t1 where object_name like '%abcde%'这种常规写法,优化器计算使用索引COST高,不会使用。只能通过改写,再配合相关hint,才能起到优化效果:

错误应对方法:

下面常规写法SQL,object_name字段上有索引,使用hint也可以强制使用该索引,但是,效率比不使用索引还要差

Select /*+ index(t1  idx_t1_object_name) */  object_id,object_type,object_name from t1 where object_name like '%ABCDE%';

正确应对方法:

如果返回的行数比较少,可以做如下改写(配合hint):

Select object_id ,object_type,object_name from t1

Where object_name in

(select /*+ cardinality(t1 5) */object_name from t1 where object_name like '%ABCDE%');

4. 索引不保存全是NULL的记录

4.1 select * from t1 where object_id is null;

这种SQL的谓词条件写法,object_id字段上的索引无法被使用,因为索引不保存全是null的条目。这种情况即使用hint也无法使用索引。

应对方法:

如果要使用索引,需要与一个非空内容做组合索引:create index idx_t1_object_id on t1(object_id,0); 原object_id字段上的索引可以删除掉。为了避免这种情况,一般索引字段在设计表时,都设计为not null,同时就会杜绝col_name is null的写法。

一个例子:

CDMBAS.BAS_PAPPVACTINFO表的PA_REQUNO , PA_ORDER 两个字段上存在复合索引,但是下面SQL却使用了全表扫描,即使使用了index的hint也不行。想想可能是什么原因?(3300万记录的表,全表扫描执行一次19秒;问题解决后使用索引只需1.7毫秒):

SELECT  B2.* FROM
        (    SELECT  B1.*,  ROWNUM rnum
                FROM  (  SELECT  
/*+ index(t1 IDX_BAS_PAPPVACTINFO_0001) */
                                        T1.PA_CLINAME    AS T1__PA_CLINAME   ,
......
                                        T1.PA_OPTIME     AS T1__PA_OPTIME
                                FROM    CDMBAS.BAS_PAPPVACTINFO T1
                                WHERE   1=1   
                                ORDER BY PA_REQUNO DESC , PA_ORDER DESC  -- 复合索引
                        ) B1
                WHERE   ROWNUM <= 21
        ) B2
WHERE   rnum > 0 ;

有索引而不能用,抛开insivisible和unusable 等情况,基本上可以确定是因为复合索引的两个字段定义都是NULL,因为索引不保存全是NULL的条目,为了保证结果的正确性,优化器不选择使用索引是正确的。

如果两个字段确实是可以为NULL,为了优化这个SQL,我们可以这样创建索引,两种方法:

  • 在联合索引的两个字段后,再增加一个定义为NOT NULL的字段,形成3个字段的联合索引
  • 直接增加一个常量作为字段,形成3个字段的联合索引
create index idx_name on CDMBAS.BAS_PAPPVACTINFO(PA_REQUNO,PA_ORDER,0);

这个案例是一个真实的生产系统,实际的业务数据中,这两个字段都是NOT NULL的,这种情况我们还有两种做法来进行优化:

  • 改写SQL,增加一个PA_REQUNO is not null的谓词条件(用另一个字段也可),即where 1=1 and PA_REQUNO is not null order by .....
  • alter table CDMBAS.BAS_PAPPVACTINFO modify (PA_REQUNO not null); 或改另一字段为not null,或者两个都改写not null,都可以。

4.2 select * from (select * from t1 order by object_id desc) where rownum<=10;

这种sql写法也与4.1一样,如果object_id 定义为not null,优化器会自动使用索引。如果定义为null,应对方法同4.1

5. 字段上使用了函数或是做了运算

下面列举了一些sql写法,这样的写法,无法使用字段上的普通索引,需要创建对应的函数索引,才能使用索引:

5.1 to_char(cdate,'yyyymmdd')='20191027'

应对方法:

如果不能改sql,只能创建函数索引: create index idx_name on tname (to_char(cdate,'yyyymmdd'));

如果可以改sql,建议改成 cdate >=to_date('20191027') and cdate

5.2 substr(name,1,10)=:b1

应对方法:

不能改sql:只能再创建函数索引;

可改写sql:name like :b1||'%' and length(:b1)=10;

5.3 cdate+1/24>sysdate 

应对方法:

不能改sql:只能再创建cdate+1/24函数索引(如果还有cdate+1/48>sysdate,还需要再创建一个cdate+1/48的函数索引)

可改写sql:需要改成 cdate>sysdate-1/24 ( cdate >sysdate - 1/48)

6. 几种隐式类型转换

6.1  varchar2类型的字段,谓词条件变量类型是number类型

phone_no=87654321 ; 这种情况,oracle会自动转换成to_number(phone_no)=87654321; 导致无法使用phone_no字段上的正常索引。

应对方法:

能改代码:

更改变量类型定义,将number类型改成varchar2类型。如果是关联字段类型不匹配(nested loop,驱动表关联字段类型是number类型,被驱动表关联字段类型是varchar2类型),则需要在nested loop驱动表的关联字段上使用to_char函数。

如: select ... from t1,t2 where t1.num_col=t2.varchar2_col

改成:select ... from t1,t2 where to_char(t1.num_col)=t2.varchar2_col

不能改代码:

创建to_number(phone_no)函数索引。

6.2  date类型的字段,谓词条件变量类型是timestamp类型:

cdate>=:b1 (b1 类型是timestamp);这种情况,oracle需要将cdate通过内部函数(internal_function)转换成timestamp类型,导致无法使用cdate字段上的正常索引。 

应对方法:

只能通过修改代码,将变量类型从timestamp改成date,或者将字段类型从date改成timestamp,不能通过创建to_timestamp函数索引解决。

6.3  varchar2类型的字段,谓词条件变量类型是nvarchar2类型

这种情况orace会自动使用SYS_OP_C2C函数,将varchar2转换成nvarchar2。执行计划中会出现SYS_OP_C2C(col_name)类似的信息(plsql developer的F5看不到这些信息)。

应对方法:

这种情况一般发生在关联字段上,可以在驱动表的关联字段上使用to_char;或者在被驱动表的关联字段上创建to_nchar或SYS_OP_C2C函数索引。

7. 绑定变量窥视与自适应游标(ACS)

与3.3类似,在分布不均字段上使用绑定变量,sql硬解析时,如果窥视到的绑定变量适合全表扫描,那么接下来使用的绑定变量如果适合使用索引,就会有多种不同选择:

  • 10g及以下版本,会一直使用全表扫描的执行计划,直到下次硬解析,再次窥视绑定变量,制定新的执行计划(可能仍是全表扫描,也可能走索引)
  • 11g及以上版本,如果关闭了自适应游标ACS,与7.1是一样的结果。
  • 11g及以上版本,如果没有关闭ACS,在绑定变量适合走索引的时候,第二次执行的时候,会变成使用索引的执行计划。
  • 11g及以上版本,如果想让绑定变量在适合走索引的时候,第一次执行时执行计划就能及时作出调整,需要使用bind_aware的hint。

应对方法:

10g版本没办法;11g版本不建议关闭ACS,即可自动完成执行计划转变,必要时需要配合bind_aware hint。

8. 直方图的局限性

11g及以下版本,

应对方法(2选1)

  • 使用hint强制使用字段上的索引。
  • 清除该字段上的直方图信息:

--清除当前直方图信息:

exec DBMS_STATS.DELETE_COLUMN_STATS('&owner','&tab_name','&col_name',col_stat_type=>'HISTOGRAM');

--避免下次收集统计信息又恢复:

exec dbms_stats.set_table_prefs( '&owner', '&tab_name', 'method_opt', 'for all columns size auto for columns size 1 &col_name');

12c及以上版本,varchar2字符串,直方图信息扩展到可以识别字符串的前64位,如果字段的前面64位都是相同的,一般不会自动收集该字段直方图信息,不影响索引的正常选择使用。如果强制收集了直方图信息,也会导致索引无法被使用。应对方法与11版本相同

9. SQL写法导致无法使用索引

下面两个sql,走object_id字段上的索引,效率都非常高:

select min(object_id) from t1;

select max(object_id) from t1;

但是,如果同时查询,只能是全表扫描(执行计划按规则办事,没办法):

select min(object_id),max(object_id) from t1;

如果字段上所有值都是NULL,则min/max函数都返回null,否则忽略null,返回非空值。在object_id定义为null时,只能是全表扫描;如果object_id 字段定义为NOT NULL,上面同时查min、max的sql,也可以使用index fast full scan的执行计划。

应对方法:

改变sql写法,让写法适应规则。写法虽然繁琐了一点,却是会带来非常大的性能提升:

select (select min(object_id) from t1) as min_object_id,

select max(object_id) from t1) as max_object_id

from dual;

思考一下,下面SQL的优化,该如果创建索引,写法又该是怎样?已知owner字段选择性差。

select max(created),min(object_id) from t1 where owner='SYS';

应对方法:

创建owner+created和owner+object_id两个索引,然后按上面方法改写(如果执行频率很低,大可不必)。

select (select min(object_id) from t1 where owner='SYS') as min_object_id,

select max(created) from t1 where owner='SYS') as max_created

from dual;

10. 谓词条件使用了or,只有部分谓词条件上有索引

select * from t1 where object_id=:b1 or object_name=:b2;

object_id和object_name两个字段选择性都不错,但是只有object_id字段上有索引,object_name字段上没有索引,这个时候是无法使用object_id字段上索引的。

应对方法:

再创建object_name字段上的索引。如果把or换成and,则两个字段任何一个字段上有索引,都会使用。

11. 函数索引中使用了常量,SQL中使用绑定变量

表上创建了如下类似函数索引:

create index idx1 on t1(substr(object_name,1,9));

create index idx2 on t1(substr(object_name,1,10));

SQL中对函数索引的常量也使用了绑定变量,这种写法是无法使用索引的

select * from t1 where substr(object_name,1,:b1)=:b2;

应对方法:

必须把函数索引中的b1绑定变量换成常量9或10。还有其他诸如 mod(xxid,10)的这种函数,如果创建了函数索引,也是不能使用绑定变量的,放在组合索引里面也只能起到过滤作用,起不到索引作用。

12. 复合索引,前导字段没有用到,且NDV(唯一值个数)较高

t1表存在两字段复合索引:object_id , object_type (object_id 唯一值个数多,选择性好)

select * from t1 where object_type='RULE';

复合索引前导字段object_id,在sql的谓词条件没有用到,这种情况不会使用索引,如果用hint强制使用索引,效率反而会更差。如果前导字段唯一值个数少,优化器会选择使用index skip scan的执行计划,效率也还可以接受,会比全表扫描好很多(mysql不支持index skip scan)。

应对方法:

创建object_type单字段索引。

说到这个情况,我再点评一个案例:

2015年oracle技术嘉年华,有个这样的案例分享(数据库是11204版本):

SQL:select * from t1 where object_type=:x and object_id=:k;

t1表130万记录,object_type和object_id两个字段上都存在单字段索引,object_id唯一值个数69万;object_type唯一值个数18,分布不均,有直方图。某天突然出现严重的性能问题,分析发现是使用了object_type字段上的索引。

原文给出的建议:

保持ACS(自适应游标)关闭,删除object_type字段上的直方图或删除object_type字段上的索引。

我对原文给出的建议都是持否定态度的:

1、正是因为关闭了ACS,才导致执行计划不能在绑定变量发生变化时不能及时调整。如果开启ACS,就不会有类似性能问题的发生。虽然acs有部分bug,但是带来的好处远大于关闭导致的最大弊端。

2、删除直方图或删除索引更是不可取,原因如下:

对于单表查询,直方图信息是优化器判断能否使用索引的重要依据,如果有sql是select * from t1 where object_type=:x ,查询的是对应记录数少的绑定变量值,这种情况是需要使用索引的,没有索引或直方图,都只能走全表扫描。此类SQL在OLTP业务也是比较常见的。

对于多表关联,直方图信息是用来准确估算Cardinality的重要依据,会影响两表关联时驱动表的选择;如果两个表关联后还要再跟其他表做关联,Cardinality估值偏差会比较大,将会导致优化器选择的执行计划可能不是最优。

13. 谓词条件是 not in(1,2) 、<> 、!=  、not like等

上面几种情况都不能使用索引,索引只能用来做等值查询或范围查询,类似上述不等于的情况,无法使用索引。

应对方法

如果经过上述谓词条件可以过滤掉大部分数据,那么可以通过改写sql+创建函数索引的方式,把不等于的sql,改写成等于,从而实现优化的目的。 详见 必须通过改写SQL才能提升性能的一些情况 第7种情况

14. 使用了sql profile

sql profile是10g版本引入的固定sql执行计划的技术,已经被DBA广泛使用。但是这个工具的原理和使用技巧,很多人还是没有完全掌握。

在检查sql执行计划时,一般建议使用dbms_xplan.display* 方法,这种方法显示的信息最全,很多DBA和开发人员喜欢用plsql developer的F5查看执行计划,会错过很多重要的信息。dbms_xplan.display*显示的执行计划,如果在note部分显示"SQL profilexxxxxxxxxxxxxx used for this statement",说明使用了sql profile。 如果sql profile中没有用索引,那么索引就不会被使用。

注: 生成sql profile一般有两种方法,一种是sql tuning advisor,这种方法生成的sql profile,还是可能随着统计信息的变化,执行计划会发生改变;另一种是使用coe_load_sql_profile.sql 脚本生成的sql profile,就不会随着统计信息的变化发生执行计划改变。

应对方法:

可以在sql增加一些注释,避开sql profile绑定的执行计划,再看看相关索引能否被使用。如果sql profile选择的执行计划不合适,可以将该sql profile 删除。

15. 使用了sql plan baseline

sql plan baseline是11g版本引入的,对于绑定变量敏感的SQL,可以设定执行计划基线,可以选择使用多个基线。用dbms_xplan.display* 显示执行计划时,如果在note部分显示"SQL Plan baseline xxxxxxxxxxxxxx used for this statement",说明使用了sql plan base line。 这种情况下,如果执行计划没有使用索引,就可能是sql plan baseline在起作用。

应对方法:

可以在sql增加一些注释,避开sql plan baseline绑定的执行计划,再看看相关索引能否被使用。如果sql plan baseline选择的执行计划不合适,可以drop掉。

16. 使用了sql patch

sql patch 是10g版本就提供的一个内部方法,使用起来还不是太方便,好在coe专家们提供了一个脚本coe_gen_sql_patch.sql,使用起来也是比较容易的。到了12.2版本,这个方法变成了一个public api,直接使用也是非常简单。

sql patch有自己的独特用途,一般不用来做固定执行计划使用,但是也可以用来控制执行计划。用dbms_xplan.display* 显示执行计划时,如果note部分显示"SQL Patch xxxxxxxxxxxxxx used for this statement",说明使用了sql patch。 

应对方法

可以在sql增加一些注释,避开sql patch绑定的执行计划,再看看相关索引能否被使用。如果sql patch选择的执行计划不合适,可以drop掉。

    以上只是列举了一部分索引(B-Tree索引)不能被使用的一些情况,应该还有一些不常见的情形,比如在字符串字段上创建了desc 降序索引,like 'xxxx%'这种sql就无法使用这个降序索引,加hint也不行;reverse key反向键索引在范围查询无法使用等,欢迎大家留言补充。同时也欢迎有识之士批评指正。

参考 

https://mp.weixin.qq.com/s/9CT96VrPn1YWWT52nnkvbA

https://mp.weixin.qq.com/s/XZnwOJ4qLGnsrB7K1n3dcw

https://mp.weixin.qq.com/s?__biz=MzIzMTQ3OTE4Mw==&mid=2247483782&idx=1&sn=2317459b698d51c75c82316872bd7db1&chksm=e8a2c366dfd54a70dc7d1c4f50b2e11326391976f117a03d698110bc65bd5dfc1c6029c317bc&scene=21#wechat_redirect

https://mp.weixin.qq.com/s?__biz=MzIzMTQ3OTE4Mw==&mid=2247483790&idx=1&sn=828d65acfab079ac2ef7b7c46c0f091d&chksm=e8a2c36edfd54a78ed9c35e1f0f9d3a6f2b26b82ada236099a393a840dc443116ddaffcc0ba6&scene=21#wechat_redirect