This note is one I drafted three years ago, based on a question from the Oracle-L. It doesn’t directly address that question because at the time I was unable to create a data set that reproduced the problem’ but it did highlight a detail that’s worth mentioning, so I’ve finally got around to completing it (and testing on a couple of newer versions of Oracle).
I’ll start with a model that was supposed to demonstrate the problem behind the question:
rem rem Script: 122_or_expand.sql rem Author: Jonathan Lewis rem Dated: Aug 2018 rem Purpose: rem rem Last tested rem 21.3.0.0 rem 19.11.0.0 rem 12.2.0.1 rem create table t1 segment creation immediate pctfree 80 pctused 20 nologging as select * from all_objects where rownum <= 50000 ; alter table t1 add constraint t1_pk primary key(object_id) using index pctfree 80 ; variable b1 number variable b2 number variable b3 number variable b4 number variable b5 number variable b6 number exec :b1 := 100 exec :b2 := 120 exec :b3 := 1100 exec :b4 := 1220 exec :b5 := 3100 exec :b6 := 3320 set serveroutput off select object_name from t1 where object_id between :b1 and :b2 or object_id between :b3 and :b4 or object_id between :b5 and :b6 ; select * from table(dbms_xplan.display_cursor(null,null,'outline'));
The critical feature of the query is the list of disjuncts (ORs) which all specify a range for object_id. The problem was that the query used a plan with an index full scan when there were no statistics on the table (or its indexes), but switched to a plan that used index range scans when statistics were gathered – and the performance of the plan with the full scan was unacceptable. (Clearly the “proper” solution is to have some suitable statistics in place – but sometimes such things are out of the control of the people who have to solve the problems.)
The /*+ index() */ and (undocumented) /*+ index_rs_asc() */ hints had no effect on the plan. The reason why the /*+ index() */ hint made no difference is because an index full scan is one of the ways in which the /*+ index() */ hint can be obeyed – the hint doesn’t instruct the optimizer to pick an index range scan. The hint /*+ index_rs_asc() */ specifically tells the optimizer to pick an index Range Scan ASCending if the hint has been specified correctly and the choice is available and legal. So why was the optimizer not doing as it was told. Without seeing the execution plan or CBO trace file from a live example I can’t guarantee that the following hypothesis is correct, but I think it’s in the right ball park.
I think the optimizer was probably using the (new to 12c) cost-based“OR expansion” transformation, which basically transformed the query into a UNION ALL of several index range scans – and that’s why its outline would show /*+ index_rs_asc() */ hints, and the hint would only become valid after the transformation had taken place so if Oracle didn’t consider (or considered and discarded) the transformation when there were no stats in place then the hint would have to be “Unused” (as the new 19c hint-report would say).
When I tried to model the problem the optimizer kept doing nice things with my data, so I wasn’t able to demonstrate the OP’s problem. However in one of my attempts to get a silly plan I did something silly – that can happen by accident if your client code isn’t careful! I’ll tell you what that was in a moment – first, a couple of plans.
As it stands, with the data and bind variables as shown, the optimizer used “b-tree / bitmap conversion” to produce an execution plan that did three separate index range scans, converts rowids to bit, OR-ed the bit-strings, then converted back to rowids before accessing the table:
--------------------------------------------------------------------------------------------- | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time | --------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | | | 84 (100)| | | 1 | TABLE ACCESS BY INDEX ROWID BATCHED| T1 | 291 | 12804 | 84 (5)| 00:00:01 | | 2 | BITMAP CONVERSION TO ROWIDS | | | | | | | 3 | BITMAP OR | | | | | | | 4 | BITMAP CONVERSION FROM ROWIDS | | | | | | | 5 | SORT ORDER BY | | | | | | |* 6 | INDEX RANGE SCAN | T1_PK | | | 2 (0)| 00:00:01 | | 7 | BITMAP CONVERSION FROM ROWIDS | | | | | | | 8 | SORT ORDER BY | | | | | | |* 9 | INDEX RANGE SCAN | T1_PK | | | 2 (0)| 00:00:01 | | 10 | BITMAP CONVERSION FROM ROWIDS | | | | | | | 11 | SORT ORDER BY | | | | | | |* 12 | INDEX RANGE SCAN | T1_PK | | | 2 (0)| 00:00:01 | ---------------------------------------------------------------------------------------------
So the first thing I had to do was disable this feature, which I did by adding the hint /*+ opt_param(‘_b_tree_bitmap_plans’,’false’) */ to the query. This adjustment left Oracle doing the OR-expansion that I didn’t want to see:
---------------------------------------------------------------------------------------------------------- | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time | ---------------------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | | | 297 (100)| | | 1 | VIEW | VW_ORE_BA8ECEFB | 288 | 19008 | 297 (1)| 00:00:01 | | 2 | UNION-ALL | | | | | | |* 3 | FILTER | | | | | | | 4 | TABLE ACCESS BY INDEX ROWID BATCHED| T1 | 18 | 792 | 20 (0)| 00:00:01 | |* 5 | INDEX RANGE SCAN | T1_PK | 18 | | 2 (0)| 00:00:01 | |* 6 | FILTER | | | | | | | 7 | TABLE ACCESS BY INDEX ROWID BATCHED| T1 | 97 | 4268 | 100 (0)| 00:00:01 | |* 8 | INDEX RANGE SCAN | T1_PK | 97 | | 2 (0)| 00:00:01 | |* 9 | FILTER | | | | | | | 10 | TABLE ACCESS BY INDEX ROWID BATCHED| T1 | 173 | 7612 | 177 (1)| 00:00:01 | |* 11 | INDEX RANGE SCAN | T1_PK | 173 | | 2 (0)| 00:00:01 | ----------------------------------------------------------------------------------------------------------
You’ll notice that the three range scans have different row estimates and costs – that’s the effect of bind variable peeking and my careful choice of bind variables to define different sized ranges. Take note, by the way, for the three filter predicates flagged at operations 3, 6, and 9. These are the “conditional plan” filters that say things like: “don’t run the sub-plan if the runtime value of :b5 is greater than :b6”.
Since I didn’t want to see OR-expansion just yet I then added the hint /*+ no_or_expand(@sel$1) */ to the query and that gave me a plan with tablescan:
-------------------------------------------------------------------------- | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time | -------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | | | 617 (100)| | |* 1 | TABLE ACCESS FULL| T1 | 291 | 12804 | 617 (4)| 00:00:01 | --------------------------------------------------------------------------
This was a shame because I really wanted to see the optimizer produce an index full scan at this point – so I decided to add an “unnamed index” hint to the growing list of hints – specifically: /*+ index_(@sel$1 t1@sel$1) */
--------------------------------------------------------------------------------------------- | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time | --------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | | | 405 (100)| | | 1 | TABLE ACCESS BY INDEX ROWID BATCHED| T1 | 291 | 12804 | 405 (2)| 00:00:01 | |* 2 | INDEX FULL SCAN | T1_PK | 291 | | 112 (7)| 00:00:01 | ---------------------------------------------------------------------------------------------
This, of course, is where things started to get a little interesting – the index full scan costs less than the tablescan but didn’t appear until hinted. But after a moment’s thought you can dismiss this one (possibly correctly) as an example of the optimizer being cautious about the cost of access paths that are dictated by bind variables or unpeekable inputs. (But these bind variables were peekable – so maybe there’s more to it than that – I was still trying to get to a point where my model would behave more like the OP’s, so I didn’t follow up on this detail: maybe in a couple of years time … ).
Once last tweak – and that will bring me to the main point of this note. In my original code I was using three ranges dictated by 3 pairs of bind variables, for example [:b5, :b6]. What would happen if I made :b5 greater than :b6, say I swapped their values?
The original btree/bitmap plan didn’t change, but where I had simply blocked bree/bitmap plans and seen OR-expansion as a result the plan changed to a full tablescan (with the cost you saw above of 617). So tried again, adding the hint /*+ or_expand(@sel$1) */ to see why; and this is the plan I got:
---------------------------------------------------------------------------------------------------------- | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time | ---------------------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | | | 735 (100)| | | 1 | VIEW | VW_ORE_BA8ECEFB | 116 | 7656 | 735 (3)| 00:00:01 | | 2 | UNION-ALL | | | | | | |* 3 | FILTER | | | | | | | 4 | TABLE ACCESS BY INDEX ROWID BATCHED| T1 | 18 | 792 | 20 (0)| 00:00:01 | |* 5 | INDEX RANGE SCAN | T1_PK | 18 | | 2 (0)| 00:00:01 | |* 6 | FILTER | | | | | | | 7 | TABLE ACCESS BY INDEX ROWID BATCHED| T1 | 97 | 4268 | 100 (0)| 00:00:01 | |* 8 | INDEX RANGE SCAN | T1_PK | 97 | | 2 (0)| 00:00:01 | |* 9 | FILTER | | | | | | |* 10 | TABLE ACCESS FULL | T1 | 1 | 44 | 615 (4)| 00:00:01 | ----------------------------------------------------------------------------------------------------------
I still get the same three branches in the expansion, but look what’s happened to the sub-plan for the third pair of bind variables. The optimizer still has the FILTER at operation 9 – and that will evaluate to FALSE for the currently peeked values; but the optimizer has decided that it should use a tablescan for this part of the query if it ever gets a pair of bind variables in the right order; and the cost of the tablescan has echoed up the plan to make the total cost of the plan 735, which is (for obvious reasons) higher than the cost of running the whole query as a single tablescan.
The same anomaly appears in 19.11.0.0 and 21.3.0.0. On the plus side, it’s possible that if you have code like this the optimizer will be using the btree/bitmap conversion anyway;
tl;dr
As a generic point it’s worth ensuring that if you’re using bind variables in client code to define ranges then you’ve got to get the values in the right order otherwise one day the answer to the question “nothing changed why is the query running so slowly?” will be “someone got in first with the bound values the wrong way round”.
[…] If you use bind variable to define ranges in query predicate make sure they are in the right order (… […]
Pingback by Execution Plans Catalogue | Oracle Scratchpad — January 28, 2022 @ 5:17 pm GMT Jan 28,2022 |