I am very often warning: dropping redundant indexes in production is not 100% safe. I have instead always been advocating paying a careful attention during design time to avoid creating redundant indexes. In my professional experience I have realized that, it is very often when creating indexes to cover the lock threat of unindexed foreign key constraints, that developers are creating unintentionally redundant indexes. It has been irritating me so that I have created a script which checks if a given foreign key is already indexed or not before creating supplementary non wanted indexes damaging the DML part of the application.
Having said that I still have not defined what is redundant indexes
“Indexes ind_1 and ind_2 are said redundant when leading columns of one index are a superset of the leading columns of the other one”
For example
Ind_1 (a,b) and ind_2(a,b,c) are redundant because ind_2 contains index ind_1.
If you are at design time it is obvious that you should not create index ind_1. However, once in production, it is not 100% safe to drop index ind_1 without any impact. There are, for sure, occasions were the clustering factor of ind_2 is so dramatic when compared to index ind_1 so that if the later index is dropped the optimizer will opt for a full table scan traumatizing the queries that were perfectly happy with the dropped redundant index.
I can also show you another type of what people might consider redundant indexes while they aren’t. Consider the following model where I have created a range partitioned table (mho_date being the partition key and I have created 1493 partitions) and two indexes as shown below
desc partitioned_tab Name Null? Type ------------------------------- -------- ------------ 1 MHO_ID NOT NULL NUMBER(10) 2 MHO_DATE NOT NULL DATE 3 MHO_CODE NOT NULL VARCHAR2(1) 4 MHO_TYP_ID NOT NULL NUMBER(10) create index local_ind_1 on partitioned_tab (mho_typ_id,mho_code) local; create index global_ind_1 on partitioned_tab (mho_typ_id);
I am going to execute a simple query against the above engineered partitioned table.
select * from partitioned_tab where mho_typ_id = 0;
Which, in the presence of the above two indexes is honored via the following execution plan
---------------------------------------------------------------------- Plan hash value: 3042058313 ---------------------------------------------------------------------------------------------------------- | Id | Operation | Name | Rows |Cost (%CPU)| Pstart| Pstop | ---------------------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1493 | 1496 (0)| | | | 1 | TABLE ACCESS BY GLOBAL INDEX ROWID BATCHED| PARTITIONED_TAB | 1493 | 1496 (0)| ROWID | ROWID | |* 2 | INDEX RANGE SCAN | GLOBAL_IND_1 | 1493 | 4 (0)| | | ---------------------------------------------------------------------------------------------------------- Predicate Information (identified by operation id): --------------------------------------------------- 2 - access("MHO_TYP_ID"=0) Statistics ------------------------------------------------------- 48 recursive calls 0 db block gets 3201 consistent gets 0 physical reads 0 redo size 51244 bytes sent via SQL*Net to client 1632 bytes received via SQL*Net from client 101 SQL*Net roundtrips to/from client 7 sorts (memory) 0 sorts (disk) 1493 rows processed
As you might already know I am one of the fans of SQLT tool developed by Carlos Sierra. And here below what this tool said about redundant indexes in this particular case
It is clearly suggesting considering dropping the redundant index global_ind_1 index
So let’s follow this advice and see what happens. Hopefully with the recent Oracle release I will first make the index invisible ( by the way that’s a good point to signal for Carlos Sierra and Mauro Pagano for them to suggest setting first the index invisible before considering dropping it)
alter index global_ind_1 invisible; select * from partitioned_tab where mho_typ_id = 0; ------------------------------------------------------------------------------------------------------------------- | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Pstart| Pstop | ------------------------------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1493 | 23888 | 2985 (1)| | | | 1 | PARTITION RANGE ALL | | 1493 | 23888 | 2985 (1)| 1 | 1493 | | 2 | TABLE ACCESS BY LOCAL INDEX ROWID BATCHED| PARTITIONED_TAB | 1493 | 23888 | 2985 (1)| 1 | 1493 | |* 3 | INDEX RANGE SCAN | LOCAL_IND_1 | 1493 | | 1492 (0)| 1 | 1493 | ------------------------------------------------------------------------------------------------------------------- Predicate Information (identified by operation id): --------------------------------------------------- 3 - access("MHO_TYP_ID"=0) Statistics -------------------------------------------------------- 48 recursive calls 0 db block gets 4588 consistent gets 0 physical reads 0 redo size 47957 bytes sent via SQL*Net to client 1632 bytes received via SQL*Net from client 101 SQL*Net roundtrips to/from client 7 sorts (memory) 0 sorts (disk) 1493 rows processed
By making the ”redundant” index invisible we went from a smooth one global index range scan with a cost of 4 to 1493 index range scans with a cost of 1492 with an additional 1387 logical reads.
Bottom line: You should always consider dropping (avoiding) redundant index at design time. Once in production consider making them invisible first before thinking carefully about dropping them.
PS : if you want to create the table the script can be found partitioned table