How do you work out what hints you need to tweak an execution plan into the shape you want?
Here’s a “case study” that’s been playing out over a few weeks on the Oracle Developer Community (here and here) and most recently ended up (in one of its versions) as a comment on one of my blog notes. It looks like a long note, but it’s a note about how to find the little bit of information you need from a large output – so it’s really a short note that has to include a long output.
Problem: a query is not running fast enough, and it runs a very large number of times in a single batch (the original trace/tkprof file reported 842,000 executions). Each individual execution, though, is very quick (as far as we know – the individual examples we have seen take a few hundredths of a second). Here’s one execution plan for the query with Query Block / Object Alias information and Outline Data pulled from memory with rowsource execution statistics enabled.
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows | Cost (%CPU)| A-Rows | A-Time | Buffers | OMem | 1Mem | Used-Mem | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | 574 (100)| 1 |00:00:00.02 | 3822 | | | | | 1 | SORT AGGREGATE | | 1 | 1 | | 1 |00:00:00.02 | 3822 | | | | |* 2 | VIEW | | 1 | 1 | 574 (2)| 0 |00:00:00.02 | 3822 | | | | |* 3 | COUNT STOPKEY | | 1 | | | 2 |00:00:00.02 | 3822 | | | | | 4 | VIEW | | 1 | 1 | 574 (2)| 2 |00:00:00.02 | 3822 | | | | |* 5 | SORT ORDER BY STOPKEY | | 1 | 1 | 574 (2)| 2 |00:00:00.02 | 3822 | 2048 | 2048 | 2048 (0)| |* 6 | FILTER | | 1 | | | 171 |00:00:00.02 | 3822 | | | | | 7 | NESTED LOOPS | | 1 | 1 | 568 (2)| 182 |00:00:00.02 | 3128 | | | | | 8 | NESTED LOOPS | | 1 | 1 | 568 (2)| 182 |00:00:00.02 | 2946 | | | | | 9 | NESTED LOOPS | | 1 | 1 | 567 (2)| 182 |00:00:00.02 | 2942 | | | | | 10 | NESTED LOOPS | | 1 | 1 | 566 (2)| 182 |00:00:00.02 | 2938 | | | | | 11 | NESTED LOOPS ANTI | | 1 | 1 | 565 (2)| 182 |00:00:00.02 | 2752 | | | | | 12 | NESTED LOOPS ANTI | | 1 | 1 | 562 (2)| 182 |00:00:00.02 | 2388 | | | | |* 13 | HASH JOIN | | 1 | 5 | 557 (2)| 182 |00:00:00.02 | 2022 | 1599K| 1599K| 1503K (0)| | 14 | VIEW | index$_join$_008 | 1 | 127 | 2 (0)| 127 |00:00:00.01 | 8 | | | | |* 15 | HASH JOIN | | 1 | | | 127 |00:00:00.01 | 8 | 1368K| 1368K| 1522K (0)| | 16 | INDEX FAST FULL SCAN | XXADM_LOVS_CODE_UK | 1 | 127 | 1 (0)| 127 |00:00:00.01 | 4 | | | | | 17 | INDEX FAST FULL SCAN | XXADM_LOVS_PK | 1 | 127 | 1 (0)| 127 |00:00:00.01 | 4 | | | | |* 18 | HASH JOIN | | 1 | 478 | 555 (2)| 182 |00:00:00.01 | 2014 | 1245K| 1245K| 1277K (0)| | 19 | NESTED LOOPS | | 1 | 478 | 243 (2)| 209 |00:00:00.01 | 883 | | | | | 20 | NESTED LOOPS | | 1 | 1 | 2 (0)| 1 |00:00:00.01 | 4 | | | | | 21 | TABLE ACCESS BY INDEX ROWID| XXADM_COLLEGE_MASTER_TBL | 1 | 1 | 1 (0)| 1 |00:00:00.01 | 2 | | | | |* 22 | INDEX UNIQUE SCAN | XXADM_COLLEGES_PK | 1 | 1 | 0 (0)| 1 |00:00:00.01 | 1 | | | | | 23 | TABLE ACCESS BY INDEX ROWID| XXADM_LOV_MASTER_TBL | 1 | 1 | 1 (0)| 1 |00:00:00.01 | 2 | | | | |* 24 | INDEX UNIQUE SCAN | XXADM_LOVS_PK | 1 | 1 | 0 (0)| 1 |00:00:00.01 | 1 | | | | |* 25 | TABLE ACCESS FULL | XXADM_APPLICANT_COURSPREFS_TBL | 1 | 478 | 241 (2)| 209 |00:00:00.01 | 879 | | | | |* 26 | TABLE ACCESS FULL | XXADM_APPLICANT_DETAILS_TBL | 1 | 6685 | 311 (2)| 10488 |00:00:00.01 | 1131 | | | | |* 27 | TABLE ACCESS BY INDEX ROWID | XXADM_APPLICANT_COURSPREFS_TBL | 182 | 8881 | 1 (0)| 0 |00:00:00.01 | 366 | | | | |* 28 | INDEX UNIQUE SCAN | XXADM_APPLCNT_PREF_ORDER_UK | 182 | 1 | 0 (0)| 182 |00:00:00.01 | 184 | | | | | 29 | VIEW PUSHED PREDICATE | VW_SQ_1 | 182 | 1 | 3 (0)| 0 |00:00:00.01 | 364 | | | | | 30 | NESTED LOOPS | | 182 | 1 | 3 (0)| 0 |00:00:00.01 | 364 | | | | |* 31 | TABLE ACCESS BY INDEX ROWID | XXADM_APPLICANT_COURSPREFS_TBL | 182 | 1 | 2 (0)| 0 |00:00:00.01 | 364 | | | | |* 32 | INDEX UNIQUE SCAN | XXADM_APPLCNT_PREF_ORDER_UK | 182 | 1 | 1 (0)| 182 |00:00:00.01 | 184 | | | | |* 33 | TABLE ACCESS BY INDEX ROWID | XXADM_CATEGORY_MASTER_TBL | 0 | 1 | 1 (0)| 0 |00:00:00.01 | 0 | | | | |* 34 | INDEX UNIQUE SCAN | XXADM_CATEGORY_PK | 0 | 1 | 0 (0)| 0 |00:00:00.01 | 0 | | | | | 35 | TABLE ACCESS BY INDEX ROWID | XXADM_LOV_MASTER_TBL | 182 | 1 | 1 (0)| 182 |00:00:00.01 | 186 | | | | |* 36 | INDEX UNIQUE SCAN | XXADM_LOVS_PK | 182 | 1 | 0 (0)| 182 |00:00:00.01 | 4 | | | | |* 37 | INDEX UNIQUE SCAN | XXADM_LOVS_PK | 182 | 1 | 0 (0)| 182 |00:00:00.01 | 4 | | | | |* 38 | INDEX UNIQUE SCAN | XXADM_LOVS_PK | 182 | 1 | 0 (0)| 182 |00:00:00.01 | 4 | | | | | 39 | TABLE ACCESS BY INDEX ROWID | XXADM_LOV_MASTER_TBL | 182 | 1 | 1 (0)| 182 |00:00:00.01 | 182 | | | | |* 40 | TABLE ACCESS BY INDEX ROWID BATCHED | XXADM_APPLICANT_COURSPREFS_TBL | 182 | 1 | 3 (0)| 29 |00:00:00.01 | 507 | | | | |* 41 | INDEX RANGE SCAN | XXADM_APPLCNT_PREFS_UK | 182 | 5 | 2 (0)| 1450 |00:00:00.01 | 191 | | | | | 42 | TABLE ACCESS BY INDEX ROWID BATCHED | XXADM_APPLICANT_COURSPREFS_TBL | 171 | 1 | 2 (0)| 0 |00:00:00.01 | 173 | | | | |* 43 | INDEX RANGE SCAN | XXADM_APPLCNT_APPLICANT_STATUS | 171 | 1 | 1 (0)| 0 |00:00:00.01 | 173 | | | | |* 44 | VIEW | index$_join$_014 | 6 | 1 | 0 (0)| 0 |00:00:00.01 | 14 | | | | |* 45 | HASH JOIN | | 6 | | | 0 |00:00:00.01 | 14 | 1519K| 1519K| 666K (0)| |* 46 | INDEX RANGE SCAN | XXADM_CATEGORY_PK | 6 | 1 | 0 (0)| 6 |00:00:00.01 | 6 | | | | | 47 | INLIST ITERATOR | | 6 | | | 12 |00:00:00.01 | 8 | | | | |* 48 | INDEX UNIQUE SCAN | XXADM_CATEGORY_CODE_UK | 12 | 1 | 0 (0)| 12 |00:00:00.01 | 8 | | | | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- Query Block Name / Object Alias (identified by operation id): ------------------------------------------------------------- 1 - SEL$1 2 - SEL$2 / from$_subquery$_001@SEL$1 3 - SEL$2 4 - SEL$7E0D484F / from$_subquery$_002@SEL$2 5 - SEL$7E0D484F 14 - SEL$082F290F / LMT_GENDER@SEL$3 15 - SEL$082F290F 16 - SEL$082F290F / indexjoin$_alias$_001@SEL$082F290F 17 - SEL$082F290F / indexjoin$_alias$_002@SEL$082F290F 21 - SEL$7E0D484F / CMT@SEL$3 22 - SEL$7E0D484F / CMT@SEL$3 23 - SEL$7E0D484F / LMT_EDUCATION_TYPE@SEL$3 24 - SEL$7E0D484F / LMT_EDUCATION_TYPE@SEL$3 25 - SEL$7E0D484F / ACT@SEL$3 26 - SEL$7E0D484F / ADT@SEL$3 27 - SEL$7E0D484F / ACT3@SEL$7 28 - SEL$7E0D484F / ACT3@SEL$7 29 - SEL$A75BE177 / VW_SQ_1@SEL$67DC521B 30 - SEL$A75BE177 31 - SEL$A75BE177 / ACT1@SEL$8 32 - SEL$A75BE177 / ACT1@SEL$8 33 - SEL$A75BE177 / XXADM_CATEGORY_MASTER_TBL@SEL$9 34 - SEL$A75BE177 / XXADM_CATEGORY_MASTER_TBL@SEL$9 35 - SEL$7E0D484F / LMT_PASS@SEL$3 36 - SEL$7E0D484F / LMT_PASS@SEL$3 37 - SEL$7E0D484F / LMT_APPEARANCE@SEL$3 38 - SEL$7E0D484F / LMT_RELIGION@SEL$3 39 - SEL$7E0D484F / LMT_RELIGION@SEL$3 40 - SEL$5 / ACT1@SEL$5 41 - SEL$5 / ACT1@SEL$5 42 - SEL$6 / ACT2@SEL$6 43 - SEL$6 / ACT2@SEL$6 44 - SEL$F665FE1B / XXADM_CATEGORY_MASTER_TBL@SEL$4 45 - SEL$F665FE1B 46 - SEL$F665FE1B / indexjoin$_alias$_001@SEL$F665FE1B 48 - SEL$F665FE1B / indexjoin$_alias$_002@SEL$F665FE1B Outline Data ------------- /*+ BEGIN_OUTLINE_DATA IGNORE_OPTIM_EMBEDDED_HINTS OPTIMIZER_FEATURES_ENABLE('12.1.0.2') DB_VERSION('12.1.0.2') OPT_PARAM('_optimizer_use_feedback' 'false') OPT_PARAM('_optimizer_dsdir_usage_control' 0) OPT_PARAM('_optimizer_adaptive_plans' 'false') OPT_PARAM('_optimizer_gather_feedback' 'false') ALL_ROWS OUTLINE_LEAF(@"SEL$F665FE1B") OUTLINE_LEAF(@"SEL$4") OUTLINE_LEAF(@"SEL$5") OUTLINE_LEAF(@"SEL$6") OUTLINE_LEAF(@"SEL$A75BE177") PUSH_PRED(@"SEL$7E0D484F" "VW_SQ_1"@"SEL$67DC521B" 16 15) OUTLINE_LEAF(@"SEL$082F290F") OUTLINE_LEAF(@"SEL$7E0D484F") UNNEST(@"SEL$9D10C90A") UNNEST(@"SEL$7") OUTLINE_LEAF(@"SEL$2") OUTLINE_LEAF(@"SEL$1") OUTLINE(@"SEL$180402DE") OUTLINE(@"SEL$7E0D484F") UNNEST(@"SEL$9D10C90A") UNNEST(@"SEL$7") OUTLINE(@"SEL$67DC521B") OUTLINE(@"SEL$9D10C90A") UNNEST(@"SEL$9") OUTLINE(@"SEL$7") OUTLINE(@"SEL$C04829E0") ELIMINATE_JOIN(@"SEL$3" "CRMT"@"SEL$3") ELIMINATE_JOIN(@"SEL$3" "MMT"@"SEL$3") OUTLINE(@"SEL$8") OUTLINE(@"SEL$9") OUTLINE(@"SEL$3") NO_ACCESS(@"SEL$1" "from$_subquery$_001"@"SEL$1") NO_ACCESS(@"SEL$2" "from$_subquery$_002"@"SEL$2") INDEX_RS_ASC(@"SEL$7E0D484F" "CMT"@"SEL$3" ("XXADM_COLLEGE_MASTER_TBL"."COLLEGE_ID")) INDEX_RS_ASC(@"SEL$7E0D484F" "LMT_EDUCATION_TYPE"@"SEL$3" ("XXADM_LOV_MASTER_TBL"."LOV_ID")) FULL(@"SEL$7E0D484F" "ACT"@"SEL$3") FULL(@"SEL$7E0D484F" "ADT"@"SEL$3") INDEX_JOIN(@"SEL$7E0D484F" "LMT_GENDER"@"SEL$3" ("XXADM_LOV_MASTER_TBL"."LOV_CODE") ("XXADM_LOV_MASTER_TBL"."LOV_ID")) INDEX_RS_ASC(@"SEL$7E0D484F" "ACT3"@"SEL$7" ("XXADM_APPLICANT_COURSPREFS_TBL"."APPLICANT_ID" "XXADM_APPLICANT_COURSPREFS_TBL"."PREFERENCE_ORDER")) NO_ACCESS(@"SEL$7E0D484F" "VW_SQ_1"@"SEL$67DC521B") INDEX_RS_ASC(@"SEL$7E0D484F" "LMT_PASS"@"SEL$3" ("XXADM_LOV_MASTER_TBL"."LOV_ID")) INDEX_RS_ASC(@"SEL$7E0D484F" "LMT_APPEARANCE"@"SEL$3" ("XXADM_LOV_MASTER_TBL"."LOV_ID")) INDEX(@"SEL$7E0D484F" "LMT_RELIGION"@"SEL$3" ("XXADM_LOV_MASTER_TBL"."LOV_ID")) LEADING(@"SEL$7E0D484F" "CMT"@"SEL$3" "LMT_EDUCATION_TYPE"@"SEL$3" "ACT"@"SEL$3" "ADT"@"SEL$3" "LMT_GENDER"@"SEL$3" "ACT3"@"SEL$7" "VW_SQ_1"@"SEL$67DC521B" "LMT_PASS"@"SEL$3" "LMT_APPEARANCE"@"SEL$3" "LMT_RELIGION"@"SEL$3") USE_NL(@"SEL$7E0D484F" "LMT_EDUCATION_TYPE"@"SEL$3") USE_NL(@"SEL$7E0D484F" "ACT"@"SEL$3") USE_HASH(@"SEL$7E0D484F" "ADT"@"SEL$3") USE_HASH(@"SEL$7E0D484F" "LMT_GENDER"@"SEL$3") USE_NL(@"SEL$7E0D484F" "ACT3"@"SEL$7") USE_NL(@"SEL$7E0D484F" "VW_SQ_1"@"SEL$67DC521B") USE_NL(@"SEL$7E0D484F" "LMT_PASS"@"SEL$3") USE_NL(@"SEL$7E0D484F" "LMT_APPEARANCE"@"SEL$3") USE_NL(@"SEL$7E0D484F" "LMT_RELIGION"@"SEL$3") NLJ_BATCHING(@"SEL$7E0D484F" "LMT_RELIGION"@"SEL$3") SWAP_JOIN_INPUTS(@"SEL$7E0D484F" "LMT_GENDER"@"SEL$3") PQ_FILTER(@"SEL$7E0D484F" SERIAL) INDEX_RS_ASC(@"SEL$A75BE177" "ACT1"@"SEL$8" ("XXADM_APPLICANT_COURSPREFS_TBL"."APPLICANT_ID" "XXADM_APPLICANT_COURSPREFS_TBL"."PREFERENCE_ORDER")) INDEX_RS_ASC(@"SEL$A75BE177" "XXADM_CATEGORY_MASTER_TBL"@"SEL$9" ("XXADM_CATEGORY_MASTER_TBL"."CATEGORY_ID")) LEADING(@"SEL$A75BE177" "ACT1"@"SEL$8" "XXADM_CATEGORY_MASTER_TBL"@"SEL$9") USE_NL(@"SEL$A75BE177" "XXADM_CATEGORY_MASTER_TBL"@"SEL$9") INDEX_RS_ASC(@"SEL$6" "ACT2"@"SEL$6" ("XXADM_APPLICANT_COURSPREFS_TBL"."APPLICANT_ID" "XXADM_APPLICANT_COURSPREFS_TBL"."STATUS_FLAG")) BATCH_TABLE_ACCESS_BY_ROWID(@"SEL$6" "ACT2"@"SEL$6") INDEX_RS_ASC(@"SEL$5" "ACT1"@"SEL$5" ("XXADM_APPLICANT_COURSPREFS_TBL"."APPLICANT_ID" "XXADM_APPLICANT_COURSPREFS_TBL"."COLLEGE_ID" "XXADM_APPLICANT_COURSPREFS_TBL"."COURSE_ID" "XXADM_APPLICANT_COURSPREFS_TBL"."MEDIUM_ID" "XXADM_APPLICANT_COURSPREFS_TBL"."HOSTEL_REQUIRED")) BATCH_TABLE_ACCESS_BY_ROWID(@"SEL$5" "ACT1"@"SEL$5") INDEX_JOIN(@"SEL$4" "XXADM_CATEGORY_MASTER_TBL"@"SEL$4" ("XXADM_CATEGORY_MASTER_TBL"."CATEGORY_ID") ("XXADM_CATEGORY_MASTER_TBL"."CATEGORY_CODE")) END_OUTLINE_DATA */
This is just one of a handful of variations that all look fairly similar and there was plenty that could be said about the query and the plan; I only want to look at one idea, though. The point came where the suggestion came to eliminate the the full tablescans at operations 25 and 26. Here’s the relevant section of the plan, stripped back a bit to make it narrower:
-------------------------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows | -------------------------------------------------------------------------------------------------------- |* 18 | HASH JOIN | | 1 | 478 | | 19 | NESTED LOOPS | | 1 | 478 | | 20 | NESTED LOOPS | | 1 | 1 | | 21 | TABLE ACCESS BY INDEX ROWID| XXADM_COLLEGE_MASTER_TBL | 1 | 1 | |* 22 | INDEX UNIQUE SCAN | XXADM_COLLEGES_PK | 1 | 1 | | 23 | TABLE ACCESS BY INDEX ROWID| XXADM_LOV_MASTER_TBL | 1 | 1 | |* 24 | INDEX UNIQUE SCAN | XXADM_LOVS_PK | 1 | 1 | |* 25 | TABLE ACCESS FULL | XXADM_APPLICANT_COURSPREFS_TBL | 1 | 478 | |* 26 | TABLE ACCESS FULL | XXADM_APPLICANT_DETAILS_TBL | 1 | 6685 | --------------------------------------------------------------------------------------------------------
To isolate the above as a relevant, self-contained, part of the plan I’ve checked that operation 26 has no child operations, and I’ve scanned up the plan to find the parent of child 26 – which turns out to be operation 18, which is a hash join with a nested loop (operation 19) as its first child and operation 26 as its second chlid.
We want to change operations 25 and 26 from full tablescans to indexed accesses; that’s the only change we need make for operation 25 which is the second table of a nested loop join, but we’ll also want to change the hash join at operation 18 into a nested loop join. To make it easy to create the right hints we start by checking the Query Block / Object Alias information to identify exactly what we’re dealing with and “where” we’re dealing with it in operations 25 and 26.
25 - SEL$7E0D484F / ACT@SEL$3 26 - SEL$7E0D484F / ADT@SEL$3
Now we can look in the Outline Data section for the hints which will say “do full tablescans on acr@sel$3 and adt@sel$3 in query block sel$7E0D484F“; and we’ll need to find a hint that tells us to do a hash join with adt4@sel$3 – and this is what we find:
FULL(@"SEL$7E0D484F" "ACT"@"SEL$3") FULL(@"SEL$7E0D484F" "ADT"@"SEL$3") USE_HASH(@"SEL$7E0D484F" "ADT"@"SEL$3")
We were a little lucky with the use_hash() hint here, as the situation could have been made a little murkier if the table we were after had also been subject to swapping join inputs (the swap_join_inputs() hint).
So all we need to do now is change those hints which (getting rid of redundant quotes, and converting to lower case because I don’t like block capitals everywhere) gives us the following:
index( @sel$7e0d484f act@sel$3 {name/definition of index}) index( @sel$7e0d484f adt@sel$3 {name/definition of index}) use_nl(@sel$7e0d484f adt@sel$3)
You have to decide your strategy for getting these hints in place, of course. Just sticking the three hints into the query probably isn’t a stable solution. Editing the outline information to include these hints (replacing the previous 3) then copying the whole outline into the query is a little messy and may not be allowed at your site. Creating an SQL Patch (with recent versions of Oracle) or an SQL Plan Baseline is probably the most appropriate strategy (possibly hacked into an SQL Profile, but I don’t like doing that). That’s a topic for another blog note, though, which I don’t need to write.
Summary
If you have a complex plan that needs a little tweaking, it’s fairly easy to find out how to change the current Outline Data to get where you want to be if you start by looking at the Query Block / Object Alias section of the plan for the operations you want to change, and then search the Outline Data for the query blocks, aliases and operations you’ve identified.