测试验证

如下SQL:

select * from test where owner='SCOTT' order by object_id;
select * from test where owner='SYS' order by object_id;

第一条SQL语句的过滤条件时where owner=‘SCOTT’,该过滤条件能过滤表中绝大部分数据。
第二条SQL语句的过滤条件时where owner=‘SYS’,该过滤条件能过滤表中一半数据。

将SQL代入正确的分页框架中并且强制走索引(object_id列索引):
select * 
 from (select * 
         from (select a.*, rownum rn from (需要分页的SQL) a) 
        where rownum <= 10) 
where rn >= 1; 

SQL>  select * from table(dbms_xplan.display_cursor(null,null,'allstats last'));

PLAN_TABLE_OUTPUT
--------------------------------------------------------------------------------------------------------------
SQL_ID  9cvywc3r6avma, child number 0
-------------------------------------
select *   from (select *           from (select a.*, rownum rn
          from (select /*+index(test idx_test)*/
    *                           from test
where owner = 'SCOTT'                          order by object_id) a)
       where rownum <= 10)  where rn >= 1

Plan hash value: 1201925926

-------------------------------------------------------------------------------------------------------

PLAN_TABLE_OUTPUT
--------------------------------------------------------------------------------------------------------------
| Id  | Operation                        | Name     | Starts | E-Rows | A-Rows |   A-Time   | Buffers |
-------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                 |          |      1 |        |     10 |00:00:00.01 |    1287 |
|*  1 |  VIEW                            |          |      1 |     10 |     10 |00:00:00.01 |    1287 |
|*  2 |   COUNT STOPKEY                  |          |      1 |        |     10 |00:00:00.01 |    1287 |
|   3 |    VIEW                          |          |      1 |     12 |     10 |00:00:00.01 |    1287 |
|   4 |     COUNT                        |          |      1 |        |     10 |00:00:00.01 |    1287 |
|   5 |      VIEW                        |          |      1 |     12 |     10 |00:00:00.01 |    1287 |
|*  6 |       TABLE ACCESS BY INDEX ROWID| TEST     |      1 |     12 |     10 |00:00:00.01 |    1287 |
|   7 |        INDEX FULL SCAN           | IDX_TEST |      1 |  75798 |  72567 |00:00:00.01 |     183 |
-------------------------------------------------------------------------------------------------------

PLAN_TABLE_OUTPUT
--------------------------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   1 - filter("RN">=1)
   2 - filter(ROWNUM<=10)
   6 - filter("OWNER"='SCOTT')
SQL>  select * from table(dbms_xplan.display_cursor(null,null,'allstats last'));

PLAN_TABLE_OUTPUT
--------------------------------------------------------------------------------------------------------------
SQL_ID  dd6hp92072cz6, child number 0
-------------------------------------
select *   from (select *           from (select a.*, rownum rn
          from (select /*+index(test idx_test)*/
    *                           from test
where owner = 'SYS'                          order by object_id) a)
     where rownum <= 10)  where rn >= 1

Plan hash value: 1201925926

-------------------------------------------------------------------------------------------------------

PLAN_TABLE_OUTPUT
--------------------------------------------------------------------------------------------------------------
| Id  | Operation                        | Name     | Starts | E-Rows | A-Rows |   A-Time   | Buffers |
-------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                 |          |      1 |        |     10 |00:00:00.01 |       5 |
|*  1 |  VIEW                            |          |      1 |     10 |     10 |00:00:00.01 |       5 |
|*  2 |   COUNT STOPKEY                  |          |      1 |        |     10 |00:00:00.01 |       5 |
|   3 |    VIEW                          |          |      1 |  35485 |     10 |00:00:00.01 |       5 |
|   4 |     COUNT                        |          |      1 |        |     10 |00:00:00.01 |       5 |
|   5 |      VIEW                        |          |      1 |  35485 |     10 |00:00:00.01 |       5 |
|*  6 |       TABLE ACCESS BY INDEX ROWID| TEST     |      1 |  35485 |     10 |00:00:00.01 |       5 |
|   7 |        INDEX FULL SCAN           | IDX_TEST |      1 |  75798 |     10 |00:00:00.01 |       3 |
-------------------------------------------------------------------------------------------------------

PLAN_TABLE_OUTPUT
--------------------------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   1 - filter("RN">=1)
   2 - filter(ROWNUM<=10)
   6 - filter("OWNER"='SYS')

从执行计划看到,两条SQL都走了INDEX FULL SCAN:
第一条SQL从索引扫描了72567行数据(Id=7 A-Rows=72567),在回表的时候对数据进行了大量过滤(Id=6),最后得到10条数据,耗费了1287个逻辑读(buffer=1287)。
第二条SQL从索引中扫描了10条数据,耗费了5个逻辑读(buffer=5)。
所以第一个执行计划是错误的,第二个是正确的,应该尽量在索引扫描的时候就取得10行数据。

差距为何这么大?
因为第一条SQL过滤条件是owner=‘SCOTT’,这个条件在表中只有很少数据,通过扫描object_id列的索引,然后回表再去匹配owner=‘SCOTT’,因为owner='SCOTT’数据量很少,要搜索大量数据才能匹配上。
而第二条SQL的过滤条件是owner=‘SYS’,因为owner='SYS’数据量多,只需要搜索少量数据就能匹配上。

创建新的索引:
对于第一条SQL,将过滤列(owner)包含在索引中,排序列是object_id:

SQL> create index idx_test_owner_id on test(owner,object_id);
Index created.
SQL>  select * from table(dbms_xplan.display_cursor(null,null,'allstats last'));

PLAN_TABLE_OUTPUT
--------------------------------------------------------------------------------------------------------------
SQL_ID  0fm4v6gtgj17a, child number 0
-------------------------------------
select *   from (select *           from (select a.*, rownum rn
          from (select /*+index(test idx_test_owner_id)*/
            *                           from test
     where owner = 'SCOTT'                          order by object_id)
a)          where rownum <= 10)  where rn >= 1

Plan hash value: 3600574413

--------------------------------------------------------------------------------------------------------------

PLAN_TABLE_OUTPUT
--------------------------------------------------------------------------------------------------------------
| Id  | Operation                        | Name              | Starts | E-Rows | A-Rows |   A-Time   | Buffers |
--------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                 |                   |      1 |        |     10 |00:00:00.01 |       6 |
|*  1 |  VIEW                            |                   |      1 |     10 |     10 |00:00:00.01 |       6 |
|*  2 |   COUNT STOPKEY                  |                   |      1 |        |     10 |00:00:00.01 |       6 |
|   3 |    VIEW                          |                   |      1 |     13 |     10 |00:00:00.01 |       6 |
|   4 |     COUNT                        |                   |      1 |        |     10 |00:00:00.01 |       6 |
|   5 |      VIEW                        |                   |      1 |     13 |     10 |00:00:00.01 |       6 |
|   6 |       TABLE ACCESS BY INDEX ROWID| TEST              |      1 |     13 |     10 |00:00:00.01 |       6 |
|*  7 |        INDEX RANGE SCAN          | IDX_TEST_OWNER_ID |      1 |     13 |     10 |00:00:00.01 |       3 |
--------------------------------------------------------------------------------------------------------------

PLAN_TABLE_OUTPUT
--------------------------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   1 - filter("RN">=1)
   2 - filter(ROWNUM<=10)
   7 - access("OWNER"='SCOTT')

从执行计划看到,SQL走了索引范围扫描,从索引中扫描了10条数据,一共耗费6个逻辑读,该执行计划是正确的。
如果在创建索引时,将object_id列在前,owner列在后:

SQL> create index idx_test_id_owner on test(object_id,owner);

Index created.

SQL>  select * from table(dbms_xplan.display_cursor(null,null,'allstats last'));

PLAN_TABLE_OUTPUT
----------------------------------------------------------------------------------------------------------------
SQL_ID  4ncks7wy73y9x, child number 0
-------------------------------------
select *   from (select *           from (select a.*, rownum rn
          from (select /*+index(test idx_test_id_owner)*/
            *                           from test
     where owner = 'SCOTT'                          order by object_id)
a)          where rownum <= 10)  where rn >= 1

Plan hash value: 2811585238

---------------------------------------------------------------------------------------------------------------

PLAN_TABLE_OUTPUT
----------------------------------------------------------------------------------------------------------------
| Id  | Operation                        | Name              | Starts | E-Rows | A-Rows |   A-Time   | Buffers |
---------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                 |                   |      1 |        |     10 |00:00:00.01 |     225 |
|*  1 |  VIEW                            |                   |      1 |     10 |     10 |00:00:00.01 |     225 |
|*  2 |   COUNT STOPKEY                  |                   |      1 |        |     10 |00:00:00.01 |     225 |
|   3 |    VIEW                          |                   |      1 |     13 |     10 |00:00:00.01 |     225 |
|   4 |     COUNT                        |                   |      1 |        |     10 |00:00:00.01 |     225 |
|   5 |      VIEW                        |                   |      1 |     13 |     10 |00:00:00.01 |     225 |
|   6 |       TABLE ACCESS BY INDEX ROWID| TEST              |      1 |     13 |     10 |00:00:00.01 |     225 |
|*  7 |        INDEX FULL SCAN           | IDX_TEST_ID_OWNER |      1 |    303 |     10 |00:00:00.01 |     222 |
---------------------------------------------------------------------------------------------------------------

PLAN_TABLE_OUTPUT
----------------------------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   1 - filter("RN">=1)
   2 - filter(ROWNUM<=10)
   7 - access("OWNER"='SCOTT')
       filter("OWNER"='SCOTT')

从执行计划看出,SQL走了索引全扫,从索引中扫描了10条数据,但是索引耗费了222个逻辑读,因为要边扫描索引边过滤数据(owner=‘SCOTT’),SQL一共耗费225个逻辑读,与走object_id列的执行计划(耗费1287个逻辑读)相比,提升了性能,但是最好的性能还是走IDX_TEST_OWNER_ID 索引(逻辑读为6)。

可不可以只在owner列创建索引?

  • 如果过滤条件能够过滤掉大部分数据(owner=‘SCOTT’),那么这时不将排序列包含在索引中也是可以的,因为只会涉及到对少量数据排序,对性能没什么影响。
  • 如果只能过滤掉一部分数据,也就是说返回数据量很多(owner=‘SYS’),这时我们必须将排序列包含在索引中,如果不将排序列包含在索引中,就需要对大量数据进行排序。在实际生产环境中,过滤条件一般都是绑定变量,无法控制传参的值,就不能确定返回数据行数究竟是多还是少,所以,建议最好将排序列包含在索引中。
  • 如果排序列有多个列,创建索引时,要将所有的排序列包含在索引中,并且要注意排序列先后顺序(语句中怎么排序,创建索引的时候就对应排序),而且还要注意列是升序还是降序。如果分页语句中排序列只有一个列,但是降序显示出来,创建索引的时候就没必要降序创建,可以使用HINT:index_desc让索引降序扫描。