Here it is a SQL query called from a .Net web service which is ”time outing” and to which I have been asked to bring a fix.
SELECT d.col_id, d.col_no, d.col_dost_id, d.col_r_id, d.xxx, d.yyy, …….. d.zzz FROM table_mho d WHERE (d.COL_UK = ‘LRBRE-12052014’ OR EXISTS (select 1 from table_mho d1 where d1.col_id = d.col_id and exists (select 1 from table_mho d2 where d2.COL_UK = ‘LRBRE-12052014’ and d1.master_col_id = d2.col_id and d2.col_type = 'M' ) and d1.col_type = 'S' ) ) order by d.col_id;
Looking carefully at the content of this query I have immediately got a clue on what might be happening here: Disjunctive Subquery.
A disjunctive subquery represents a subquery that appears in an OR predicate (disjunction). And the above query has indeed an OR predicate followed by an EXISTS clause:
OR EXISTS (select 1 from table_mho d1 where d1.col_id = d.col_id and exists (select 1 from table_mho d2 where d2.COL_UK = ‘LRBRE-12052014’ and d1.master_col_id = d2.col_id and d2.col_type = 'M' ) and d1.col_type = 'S' )
I am not going to dig in the details of disjunctive subqueries and their inability to be unnested by the CBO for releases prior to 12c. I will be writing in a near future (I hope) a general article in which disjunctive subqueries will be explained and popularized via a reproducible model. The goal of this brief blog post is just to show how I have been successful to trouble shoot the above web service performance issue by transforming a disjunctive subquery into an UNION ALL SQL statement so that I gave the CBO an opportunity to choose an optimal plan.
Here it is the sub-optimal plan for the original query
--------------------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | --------------------------------------------------------------------------------------------------- | 1 | SORT ORDER BY | | 1 | 46854 | 1 |00:00:10.70 | |* 2 | FILTER | | 1 | | 1 |00:00:10.70 | | 3 | TABLE ACCESS FULL | TABLE_MHO | 1 | 937K| 937K|00:00:00.94 | | 4 | NESTED LOOPS | | 937K| 1 | 0 |00:00:07.26 | |* 5 | TABLE ACCESS BY INDEX ROWID| TABLE_MHO | 937K| 1 | 60 |00:00:06.65 | |* 6 | INDEX UNIQUE SCAN | COL_MHO_PK | 937K| 1 | 937K|00:00:04.14 | |* 7 | TABLE ACCESS BY INDEX ROWID| TABLE_MHO | 60 | 1 | 0 |00:00:00.01 | |* 8 | INDEX UNIQUE SCAN | COL_MHO_UK | 60 | 1 | 60 |00:00:00.01 | --------------------------------------------------------------------------------------------------- Predicate Information (identified by operation id): --------------------------------------------------- 2 - filter(("D"."COL_UK"=‘LRBRE-12052014’ OR IS NOT NULL)) 5 - filter(("D1"."MASTER_COL_ID" IS NOT NULL AND "D1"."COL_TYPE"='S')) 6 - access("D1"."COL_ID"=:B1) 7 - filter(("D2"."COL_TYPE"='M' AND "D1"."MASTER_COL_ID"="D2"."COL_ID")) 8 - access("D2"."COL_UK"=‘LRBRE-12052014’) Statistics --------------------------------------------------- 0 recursive calls 0 db block gets 3771234 consistent gets 22748 physical reads 0 redo size 1168 bytes sent via SQL*Net to client 244 bytes received via SQL*Net from client 2 SQL*Net roundtrips to/from client 1 sorts (memory) 0 sorts (disk) 1 rows processed
Note the apparition of the FILTER operation (n°2) which is less efficient. One of the dramatic consequences of that is the NESTED LOOP operation (n°4) which has been started 937,000 times without producing any rows but nevertheless generating almost 4 millions of buffer gets. Because of this disjunctive subquery, Oracle is not able to merge the subquery clause with the rest of the query in order to consider another optimal path.
There is a simple technique if you want to re-write the above query in order to get rid of the disjunctive subquery: use of an UNION ALL as I did for my original query (bear in mind that in my actual case COL_UK column is NOT NULL)
SELECT ww.** FROM (SELECT d.col_id, d.col_no, d.col_dost_id, d.col_r_id, d.xxx, d.yyy, …….. d.zzz FROM table_mho d WHERE d.COL_UK = ‘LRBRE-12052014’ UNION ALL SELECT d.col_id, d.col_no, d.col_dost_id, d.col_r_id, d.xxx, d.yyy, …….. d.zzz FROM table_mho d WHERE d.COL_UK != ‘LRBRE-12052014’ AND EXISTS (select 1 from table_mho d1 where d1.col_id = d.col_id and exists (select 1 from table_mho d2 where d2.COL_UK = ‘LRBRE-12052014’ and d1.master_col_id = d2.col_id and d2.col_type = 'M' ) and d1.col_type = 'S' ) ) ) ww order by ww.col_id;
And here it is the new corresponding optimal plan
------------------------------------------------------------------------------------------------------------ | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | ------------------------------------------------------------------------------------------------------------ | 1 | SORT ORDER BY | | 1 | 2 | 1 |00:00:00.01 | | 2 | VIEW | | 1 | 2 | 1 |00:00:00.01 | | 3 | UNION-ALL | | 1 | | 1 |00:00:00.01 | | 4 | TABLE ACCESS BY INDEX ROWID | TABLE_MHO | 1 | 1 | 1 |00:00:00.01 | |* 5 | INDEX UNIQUE SCAN | COL_MHO_UK | 1 | 1 | 1 |00:00:00.01 | | 6 | NESTED LOOPS | | 1 | 1 | 0 |00:00:00.01 | | 7 | VIEW | VW_SQ_1 | 1 | 1 | 0 |00:00:00.01 | | 8 | HASH UNIQUE | | 1 | 1 | 0 |00:00:00.01 | | 9 | NESTED LOOPS | | 1 | 1 | 0 |00:00:00.01 | |* 10 | TABLE ACCESS BY INDEX ROWID| TABLE_MHO | 1 | 1 | 0 |00:00:00.01 | |* 11 | INDEX UNIQUE SCAN | COL_MHO_UK | 1 | 1 | 1 |00:00:00.01 | |* 12 | TABLE ACCESS BY INDEX ROWID| TABLE_MHO | 0 | 1 | 0 |00:00:00.01 | |* 13 | INDEX RANGE SCAN | COL_COL_MHO_FK_I | 0 | 62 | 0 |00:00:00.01 | |* 14 | TABLE ACCESS BY INDEX ROWID | TABLE_MHO | 0 | 1 | 0 |00:00:00.01 | |* 15 | INDEX UNIQUE SCAN | COL_MHO_PK | 0 | 1 | 0 |00:00:00.01 | ------------------------------------------------------------------------------------------------------------ Predicate Information (identified by operation id): --------------------------------------------------- 5 - access("D"."COL_UK"=‘LRBRE-12052014’) 10 - filter("D2"."COL_TYPE"='M') 11 - access("D2"."COL_UK"=‘LRBRE-12052014’) 12 - filter("D1"."COL_TYPE"='S') 13 - access("D1"."MASTER_COL_ID"="D2"."COL_ID") filter("D1"."MASTER_COL_ID" IS NOT NULL) 14 - filter("D"."COL_UK"<>‘LRBRE-12052014’) 15 - access("COL_ID"="D"."COL_ID") Statistics ------------------------------------------------------ 0 recursive calls 0 db block gets 8 consistent gets 0 physical reads 0 redo size 1168 bytes sent via SQL*Net to client 403 bytes received via SQL*Net from client 2 SQL*Net roundtrips to/from client 1 sorts (memory) 0 sorts (disk) 1 rows processed
I went from a massif 3,771,234 of consistent gets to just 8 logical I/O. The subquery has been transformed into an in-line view (VW_SQ_1) while the not always good FILTER operation disappeared letting the place to rapid operations accurately estimated by the CBO without re-computing the statistics in between
Bottom Line: Trouble shooting a query performance problem can sometime be achieved by reformulating the query so that you give the CBO a way to circumvent a transformation it can’t do with the original query.