Index Sanity in Oracle

Unused indexes are a major topic for Oracle DBAs, since each index a table has increases the maintenance “cost” of inserts, updates and deletes. Many a blog post has been written providing ways to find such ‘unused’ indexes, many relying on a relatively newer Oracle feature, invisible indexes. Two other articles discuss this, one an effort to help the DBA discover unused indexes and the other discusses an issue where those invisible indexes aren’t totally invisible. Neither one shows what could happen when an index, thought to be unused and dropped, really wasn’t. [Invisible indexes were not used in this example, the indexes were simiply dropped based upon the execution plan generated.] Depending on the release of Oracle in use the behaviour can be slightly different; we’ll look at 11.2.0.4 and 12.1.0.2 and show the behavior of each. The example used is modified from one provided by Jonathan Lewis and it illustrates the point quite well. Let’s get this party started.

The example creates three tables, populates them, creates indexes and then performs a join between them. Looking at the original execution plan provides the basis for the index evaluation and eventual drop. The queries run afterwards illustrate that the dropped index WAS being used by the optimizer even though it wasn’t mentioned in the plan. Proof of this will be recorded by the 10053 traces generated for each session.

Using Oracle 11.2.0.4 the following results are obtained:


SQL> alter session set tracefile_identifier='index_sanity';

Session altered.

SQL> alter session set events='10053 trace name context forever, level 2';

Session altered.

SQL>
SQL> select
  2  	     t1.small_vc, t2.small_vc, t3.small_vc
  3  from
  4  	     t1, t2, t3
  5  where
  6  	     t1.n1 between 40 and 50
  7  and     t2.id1 = t1.id1
  8  and     t2.ind_pad = t1.ind_pad
  9  and     t2.id2 = t1.id2
 10  and     t3.id = t1.id1
 11  ;

SMALL_VC   SMALL_VC   SMALL_VC
---------- ---------- ----------
0000003941 0000000001 1
0000003841 0000000001 1
0000003741 0000000001 1
0000003641 0000000001 1
0000003541 0000000001 1
0000003441 0000000001 1
0000003341 0000000001 1
...
0000000749 0000000009 9
0000000649 0000000009 9
0000000549 0000000009 9
0000000449 0000000009 9
0000000349 0000000009 9
0000000249 0000000009 9
0000000149 0000000009 9
0000000049 0000000009 9

360 rows selected.


Execution Plan
----------------------------------------------------------
Plan hash value: 1184213596

----------------------------------------------------------------------------
| Id  | Operation           | Name | Rows  | Bytes | Cost (%CPU)| Time     |
----------------------------------------------------------------------------
|   0 | SELECT STATEMENT    |      |   484 | 64856 |   481   (1)| 00:00:06 |
|*  1 |  HASH JOIN          |      |   484 | 64856 |   481   (1)| 00:00:06 |
|*  2 |   HASH JOIN         |      |   484 | 57596 |    27   (0)| 00:00:01 |
|   3 |    TABLE ACCESS FULL| T2   |    20 |  1160 |     7   (0)| 00:00:01 |
|*  4 |    TABLE ACCESS FULL| T1   |   484 | 29524 |    20   (0)| 00:00:01 |
|   5 |   TABLE ACCESS FULL | T3   |  5000 | 75000 |   454   (1)| 00:00:06 |
----------------------------------------------------------------------------

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

   1 - access("T3"."ID"="T1"."ID1")
   2 - access("T2"."ID1"="T1"."ID1" AND "T2"."IND_PAD"="T1"."IND_PAD"
              AND "T2"."ID2"="T1"."ID2")
   4 - filter("T1"."N1"<=50 AND "T1"."N1">=40)

SQL>
SQL> drop index t1_i1;

Index dropped.

SQL>
SQL> accept X prompt "Press return to continue"
Press return to continue
SQL>
SQL> select
  2  	     t1.small_vc, t2.small_vc, t3.small_vc
  3  from
  4  	     t1, t2, t3
  5  where
  6  	     t1.n1 between 40 and 50
  7  and     t2.id1 = t1.id1
  8  and     t2.ind_pad = t1.ind_pad
  9  and     t2.id2 = t1.id2
 10  and     t3.id = t1.id1
 11  ;

SMALL_VC   SMALL_VC   SMALL_VC
---------- ---------- ----------
0000000041 0000000001 1
0000000042 0000000002 2
0000000043 0000000003 3
0000000044 0000000004 4
0000000045 0000000005 5
0000000046 0000000006 6
...
0000003942 0000000002 2
0000003943 0000000003 3
0000003944 0000000004 4
0000003945 0000000005 5
0000003946 0000000006 6
0000003947 0000000007 7
0000003948 0000000008 8
0000003949 0000000009 9

360 rows selected.


Execution Plan
----------------------------------------------------------
Plan hash value: 2290830436

--------------------------------------------------------------------------------------
| Id  | Operation                    | Name  | Rows  | Bytes | Cost (%CPU)| Time     |
--------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT             |       |    52 |  6968 |    79   (0)| 00:00:01 |
|   1 |  NESTED LOOPS                |       |    52 |  6968 |    79   (0)| 00:00:01 |
|   2 |   NESTED LOOPS               |       |    52 |  6968 |    79   (0)| 00:00:01 |
|*  3 |    HASH JOIN                 |       |    52 |  6188 |    27   (0)| 00:00:01 |
|   4 |     TABLE ACCESS FULL        | T2    |    20 |  1160 |     7   (0)| 00:00:01 |
|*  5 |     TABLE ACCESS FULL        | T1    |   484 | 29524 |    20   (0)| 00:00:01 |
|*  6 |    INDEX UNIQUE SCAN         | T3_PK |     1 |       |     0   (0)| 00:00:01 |
|   7 |   TABLE ACCESS BY INDEX ROWID| T3    |     1 |    15 |     1   (0)| 00:00:01 |
--------------------------------------------------------------------------------------

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

   3 - access("T2"."ID1"="T1"."ID1" AND "T2"."IND_PAD"="T1"."IND_PAD" AND
              "T2"."ID2"="T1"."ID2")
   5 - filter("T1"."N1"<=50 AND "T1"."N1">=40)
   6 - access("T3"."ID"="T1"."ID1")

SQL>
SQL> accept X prompt "Press return to continue"
Press return to continue
SQL>
SQL> create	   index t1_i1 on t1(id1, ind_pad, id2) pctfree 91;

Index created.

SQL>
SQL> drop index t2_i1;

Index dropped.

SQL>
SQL> select
  2  	     t1.small_vc, t2.small_vc, t3.small_vc
  3  from
  4  	     t1, t2, t3
  5  where
  6  	     t1.n1 between 40 and 50
  7  and     t2.id1 = t1.id1
  8  and     t2.ind_pad = t1.ind_pad
  9  and     t2.id2 = t1.id2
 10  and     t3.id = t1.id1
 11  ;

SMALL_VC   SMALL_VC   SMALL_VC
---------- ---------- ----------
0000000041 0000000001 1
0000000042 0000000002 2
0000000043 0000000003 3
0000000044 0000000004 4
0000000045 0000000005 5
0000000046 0000000006 6
0000000047 0000000007 7
...
0000003942 0000000002 2
0000003943 0000000003 3
0000003944 0000000004 4
0000003945 0000000005 5
0000003946 0000000006 6
0000003947 0000000007 7
0000003948 0000000008 8
0000003949 0000000009 9

360 rows selected.


Execution Plan
----------------------------------------------------------
Plan hash value: 2290830436

--------------------------------------------------------------------------------------
| Id  | Operation                    | Name  | Rows  | Bytes | Cost (%CPU)| Time     |
--------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT             |       |    52 |  6968 |    79   (0)| 00:00:01 |
|   1 |  NESTED LOOPS                |       |    52 |  6968 |    79   (0)| 00:00:01 |
|   2 |   NESTED LOOPS               |       |    52 |  6968 |    79   (0)| 00:00:01 |
|*  3 |    HASH JOIN                 |       |    52 |  6188 |    27   (0)| 00:00:01 |
|   4 |     TABLE ACCESS FULL        | T2    |    20 |  1160 |     7   (0)| 00:00:01 |
|*  5 |     TABLE ACCESS FULL        | T1    |   484 | 29524 |    20   (0)| 00:00:01 |
|*  6 |    INDEX UNIQUE SCAN         | T3_PK |     1 |       |     0   (0)| 00:00:01 |
|   7 |   TABLE ACCESS BY INDEX ROWID| T3    |     1 |    15 |     1   (0)| 00:00:01 |
--------------------------------------------------------------------------------------

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

   3 - access("T2"."ID1"="T1"."ID1" AND "T2"."IND_PAD"="T1"."IND_PAD" AND
              "T2"."ID2"="T1"."ID2")
   5 - filter("T1"."N1"<=50 AND "T1"."N1">=40)
   6 - access("T3"."ID"="T1"."ID1")

SQL>
SQL> alter session set events='10053 trace name context off';

Session altered.

The 10053 trace file from the 11.2.0.4 run shows this for the first query:


***************************************
BASE STATISTICAL INFORMATION
***********************
Table Stats::
  Table: T3  Alias: T3
    #Rows: 5000  #Blks:  1667  AvgRowLen:  116.00  ChainCnt:  0.00
  Column (#1): ID(
    AvgLen: 4 NDV: 5000 Nulls: 0 Density: 0.000200 Min: 1 Max: 5000
Index Stats::
  Index: T3_PK  Col#: 1
    LVLS: 1  #LB: 10  #DK: 5000  LB/K: 1.00  DB/K: 1.00  CLUF: 1667.00
***********************
Table Stats::
  Table: T2  Alias: T2
    #Rows: 20  #Blks:  20  AvgRowLen:  262.00  ChainCnt:  0.00
  Column (#1): ID1(
    AvgLen: 3 NDV: 10 Nulls: 0 Density: 0.100000 Min: 0 Max: 9
  Column (#3): IND_PAD(
    AvgLen: 41 NDV: 1 Nulls: 0 Density: 1.000000
  Column (#2): ID2(
    AvgLen: 3 NDV: 20 Nulls: 0 Density: 0.050000 Min: 0 Max: 19
Index Stats::
  Index: T2_I1  Col#: 1 3 2
    LVLS: 1  #LB: 3  #DK: 20  LB/K: 1.00  DB/K: 1.00  CLUF: 20.00
***********************
Table Stats::
  Table: T1  Alias: T1
    #Rows: 4000  #Blks:  66  AvgRowLen:  112.00  ChainCnt:  0.00
  Column (#1): ID1(
    AvgLen: 3 NDV: 10 Nulls: 0 Density: 0.100000 Min: 0 Max: 9
  Column (#3): IND_PAD(
    AvgLen: 41 NDV: 1 Nulls: 0 Density: 1.000000
  Column (#2): ID2(
    AvgLen: 3 NDV: 20 Nulls: 0 Density: 0.050000 Min: 0 Max: 19
Index Stats::
  Index: T1_I1  Col#: 1 3 2
    LVLS: 2  #LB: 443  #DK: 20  LB/K: 22.00  DB/K: 66.00  CLUF: 1320.00
***************************************

Even though the optimizer elected to use full table scans to access the data the index statistics were used to make that decision. For the second query (after the index drop) the optimizer reports these statistics:


***************************************
BASE STATISTICAL INFORMATION
***********************
Table Stats::
  Table: T3  Alias: T3
    #Rows: 5000  #Blks:  1667  AvgRowLen:  116.00  ChainCnt:  0.00
  Column (#1): ID(
    AvgLen: 4 NDV: 5000 Nulls: 0 Density: 0.000200 Min: 1 Max: 5000
Index Stats::
  Index: T3_PK  Col#: 1
    LVLS: 1  #LB: 10  #DK: 5000  LB/K: 1.00  DB/K: 1.00  CLUF: 1667.00
***********************
Table Stats::
  Table: T2  Alias: T2
    #Rows: 20  #Blks:  20  AvgRowLen:  262.00  ChainCnt:  0.00
  Column (#1): ID1(
    AvgLen: 3 NDV: 10 Nulls: 0 Density: 0.100000 Min: 0 Max: 9
  Column (#3): IND_PAD(
    AvgLen: 41 NDV: 1 Nulls: 0 Density: 1.000000
  Column (#2): ID2(
    AvgLen: 3 NDV: 20 Nulls: 0 Density: 0.050000 Min: 0 Max: 19
Index Stats::
  Index: T2_I1  Col#: 1 3 2
    LVLS: 1  #LB: 3  #DK: 20  LB/K: 1.00  DB/K: 1.00  CLUF: 20.00
***********************
Table Stats::
  Table: T1  Alias: T1
    #Rows: 4000  #Blks:  66  AvgRowLen:  112.00  ChainCnt:  0.00
  Column (#1): ID1(
    AvgLen: 3 NDV: 10 Nulls: 0 Density: 0.100000 Min: 0 Max: 9
  Column (#3): IND_PAD(
    AvgLen: 41 NDV: 1 Nulls: 0 Density: 1.000000
  Column (#2): ID2(
    AvgLen: 3 NDV: 20 Nulls: 0 Density: 0.050000 Min: 0 Max: 19
***************************************

This now causes the index on table T3 to ‘kick in’. The interesting point in this example is that dropping the index on table T1 or T2 (keeping the other index in place) produces the same plan.

Using Oracle 12.1.0.2 similar behavior results:


SQL> alter session set tracefile_identifier='index_sanity';

Session altered.

SQL> alter session set events='10053 trace name context forever, level 2';

Session altered.

SQL>
SQL> select
  2  	     t1.small_vc, t2.small_vc, t3.small_vc
  3  from
  4  	     t1, t2, t3
  5  where
  6  	     t1.n1 between 40 and 50
  7  and     t2.id1 = t1.id1
  8  and     t2.ind_pad = t1.ind_pad
  9  and     t2.id2 = t1.id2
 10  and     t3.id = t1.id1
 11  ;

SMALL_VC   SMALL_VC   SMALL_VC
---------- ---------- ----------
0000000041 0000000001 1
0000000042 0000000002 2
0000000043 0000000003 3
0000000044 0000000004 4
0000000045 0000000005 5
0000000046 0000000006 6
...
0000003942 0000000002 2
0000003943 0000000003 3
0000003944 0000000004 4
0000003945 0000000005 5
0000003946 0000000006 6
0000003947 0000000007 7
0000003948 0000000008 8
0000003949 0000000009 9

360 rows selected.


Execution Plan
----------------------------------------------------------
Plan hash value: 1184213596

----------------------------------------------------------------------------
| Id  | Operation           | Name | Rows  | Bytes | Cost (%CPU)| Time     |
----------------------------------------------------------------------------
|   0 | SELECT STATEMENT    |      |   484 | 64856 |   481   (1)| 00:00:01 |
|*  1 |  HASH JOIN          |      |   484 | 64856 |   481   (1)| 00:00:01 |
|*  2 |   HASH JOIN         |      |   484 | 57596 |    27   (0)| 00:00:01 |
|   3 |    TABLE ACCESS FULL| T2   |    20 |  1160 |     7   (0)| 00:00:01 |
|*  4 |    TABLE ACCESS FULL| T1   |   484 | 29524 |    20   (0)| 00:00:01 |
|   5 |   TABLE ACCESS FULL | T3   |  5000 | 75000 |   454   (1)| 00:00:01 |
----------------------------------------------------------------------------

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

   1 - access("T3"."ID"="T1"."ID1")
   2 - access("T2"."ID1"="T1"."ID1" AND "T2"."IND_PAD"="T1"."IND_PAD"
              AND "T2"."ID2"="T1"."ID2")
   4 - filter("T1"."N1"<=50 AND "T1"."N1">=40)

Note
-----
   - this is an adaptive plan

SQL>
SQL> drop index t1_i1;

Index dropped.

SQL>
SQL> accept X prompt "Press return to continue"
Press return to continue
SQL>
SQL> select
  2  	     t1.small_vc, t2.small_vc, t3.small_vc
  3  from
  4  	     t1, t2, t3
  5  where
  6  	     t1.n1 between 40 and 50
  7  and     t2.id1 = t1.id1
  8  and     t2.ind_pad = t1.ind_pad
  9  and     t2.id2 = t1.id2
 10  and     t3.id = t1.id1
 11  ;

SMALL_VC   SMALL_VC   SMALL_VC
---------- ---------- ----------
0000000041 0000000001 1
0000000042 0000000002 2
0000000043 0000000003 3
0000000044 0000000004 4
0000000045 0000000005 5
0000000046 0000000006 6
...
0000003942 0000000002 2
0000003943 0000000003 3
0000003944 0000000004 4
0000003945 0000000005 5
0000003946 0000000006 6
0000003947 0000000007 7
0000003948 0000000008 8
0000003949 0000000009 9

360 rows selected.


Execution Plan
----------------------------------------------------------
Plan hash value: 2290830436

--------------------------------------------------------------------------------------
| Id  | Operation                    | Name  | Rows  | Bytes | Cost (%CPU)| Time     |
--------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT             |       |    52 |  6968 |    79   (0)| 00:00:01 |
|   1 |  NESTED LOOPS                |       |    52 |  6968 |    79   (0)| 00:00:01 |
|   2 |   NESTED LOOPS               |       |    52 |  6968 |    79   (0)| 00:00:01 |
|*  3 |    HASH JOIN                 |       |    52 |  6188 |    27   (0)| 00:00:01 |
|   4 |     TABLE ACCESS FULL        | T2    |    20 |  1160 |     7   (0)| 00:00:01 |
|*  5 |     TABLE ACCESS FULL        | T1    |   484 | 29524 |    20   (0)| 00:00:01 |
|*  6 |    INDEX UNIQUE SCAN         | T3_PK |     1 |       |     0   (0)| 00:00:01 |
|   7 |   TABLE ACCESS BY INDEX ROWID| T3    |     1 |    15 |     1   (0)| 00:00:01 |
--------------------------------------------------------------------------------------

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

   3 - access("T2"."ID1"="T1"."ID1" AND "T2"."IND_PAD"="T1"."IND_PAD" AND
              "T2"."ID2"="T1"."ID2")
   5 - filter("T1"."N1"<=50 AND "T1"."N1">=40)
   6 - access("T3"."ID"="T1"."ID1")

Note
-----
   - this is an adaptive plan

SQL>
SQL> accept X prompt "Press return to continue"
Press return to continue
SQL>
SQL> create	   index t1_i1 on t1(id1, ind_pad, id2) pctfree 91;

Index created.

SQL>
SQL> drop index t2_i1;

Index dropped.

SQL>
SQL> select
  2  	     t1.small_vc, t2.small_vc, t3.small_vc
  3  from
  4  	     t1, t2, t3
  5  where
  6  	     t1.n1 between 40 and 50
  7  and     t2.id1 = t1.id1
  8  and     t2.ind_pad = t1.ind_pad
  9  and     t2.id2 = t1.id2
 10  and     t3.id = t1.id1
 11  ;

SMALL_VC   SMALL_VC   SMALL_VC
---------- ---------- ----------
0000000041 0000000001 1
0000000042 0000000002 2
0000000043 0000000003 3
0000000044 0000000004 4
0000000045 0000000005 5
0000000046 0000000006 6
...
0000003942 0000000002 2
0000003943 0000000003 3
0000003944 0000000004 4
0000003945 0000000005 5
0000003946 0000000006 6
0000003947 0000000007 7
0000003948 0000000008 8
0000003949 0000000009 9

360 rows selected.


Execution Plan
----------------------------------------------------------
Plan hash value: 2290830436

--------------------------------------------------------------------------------------
| Id  | Operation                    | Name  | Rows  | Bytes | Cost (%CPU)| Time     |
--------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT             |       |    52 |  6968 |    79   (0)| 00:00:01 |
|   1 |  NESTED LOOPS                |       |    52 |  6968 |    79   (0)| 00:00:01 |
|   2 |   NESTED LOOPS               |       |    52 |  6968 |    79   (0)| 00:00:01 |
|*  3 |    HASH JOIN                 |       |    52 |  6188 |    27   (0)| 00:00:01 |
|   4 |     TABLE ACCESS FULL        | T2    |    20 |  1160 |     7   (0)| 00:00:01 |
|*  5 |     TABLE ACCESS FULL        | T1    |   484 | 29524 |    20   (0)| 00:00:01 |
|*  6 |    INDEX UNIQUE SCAN         | T3_PK |     1 |       |     0   (0)| 00:00:01 |
|   7 |   TABLE ACCESS BY INDEX ROWID| T3    |     1 |    15 |     1   (0)| 00:00:01 |
--------------------------------------------------------------------------------------

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

   3 - access("T2"."ID1"="T1"."ID1" AND "T2"."IND_PAD"="T1"."IND_PAD" AND
              "T2"."ID2"="T1"."ID2")
   5 - filter("T1"."N1"<=50 AND "T1"."N1">=40)
   6 - access("T3"."ID"="T1"."ID1")

Note
-----
   - this is an adaptive plan

SQL>
SQL> alter session set events='10053 trace name context off';

Session altered.

The trace file for the 12.1.0.2 session reports the same data as the trace from 11.2.0.4 so that won’t be provided here.

Thinking an index is unused and proving it are two different things, and as can be seen from this example it’s not an easy task without using a 10053 trace to see exactly what statistics the optimizer is using to make its decisions. Given the small volume of data in this example it’s hard to say which is the better plan; larger volumes of data would be required to make that determination. This example does prove, however, that the execution plan alone cannot be relied upon to provide all of the required information to determine if an index is or is not used.

There can be many ways to try to determine if an index is being used or not; execution plans, using invisible indexes (with caution), using a 10053 trace to view the optimizer decision tree although that last option can be a bit tedious depending on the index configuration for the table or tables in question. It is worth the time invested to ensure that what you think is happening is actually happening, so there won’t be any surprises, for the DBA or the users, later.

See all articles by David Fitzjarrell

David Fitzjarrell
David Fitzjarrell
David Fitzjarrell has more than 20 years of administration experience with various releases of the Oracle DBMS. He has installed the Oracle software on many platforms, including UNIX, Windows and Linux, and monitored and tuned performance in those environments. He is knowledgeable in the traditional tools for performance tuning – the Oracle Wait Interface, Statspack, event 10046 and 10053 traces, tkprof, explain plan and autotrace – and has used these to great advantage at the U.S. Postal Service, American Airlines/SABRE, ConocoPhilips and SiriusXM Radio, among others, to increase throughput and improve the quality of the production system. He has also set up scripts to regularly monitor available space and set thresholds to notify DBAs of impending space shortages before they affect the production environment. These scripts generate data which can also used to trend database growth over time, aiding in capacity planning. He has used RMAN, Streams, RAC and Data Guard in Oracle installations to ensure full recoverability and failover capabilities as well as high availability, and has configured a 'cascading' set of DR databases using the primary DR databases as the source, managing the archivelog transfers manually and montoring, through scripts, the health of these secondary DR databases. He has also used ASM, ASMM and ASSM to improve performance and manage storage and shared memory.

Get the Free Newsletter!

Subscribe to Cloud Insider for top news, trends & analysis

Latest Articles