I’m afraid this is one of my bad puns again – an example of the optimizer making a real hash of the index hash join. I’m going to create a table with several indexes (some of them rather similar to each other) and execute a query that should do an index join between the obvious two indexes. To show how obvious the join should be I’m going to start with a couple of queries that show the cost of simple index fast full scans.
Here’s the data generating code:
rem rem Script: index_hash.sql rem Author: Jonathan Lewis rem Dated: Sept 2011 rem create table t1 as with generator as ( select rownum id from dual connect by level <= 1e4 -- > comment to avoid wordpress format issue ) select rownum id, mod(rownum-1,20) flag, lpad(rownum,10,'0') v10, lpad(rownum,20,'0') v20, lpad(rownum,30,'0') v30, rpad('x',100) padding from generator v1, generator v2 where rownum <= 1e5 -- > comment to avoid wordpress format issue ; begin dbms_stats.gather_table_stats( ownname => user, tabname =>'T1', estimate_percent => 100, method_opt => 'for all columns size 1' ); end; / alter table t1 add constraint t1_pk primary key(id) using index (create index t1_pk on t1(id)) ; create index t1_flag on t1(flag); create index t1_ia on t1(id, v20); create index t1_ib on t1(id, v10); create index t1_ic on t1(id, v30); select index_name, leaf_blocks from user_indexes where table_name = 'T1' order by index_name ; /* output from the query */ /* INDEX_NAME LEAF_BLOCKS -------------------- ----------- T1_FLAG 195 T1_IA 515 T1_IB 375 T1_IC 657 T1_PK 222 */
Given the definitions of the primary key index and the three indexes that start with the ID column their relative sizes shouldn’t surprise you. The cost of an index fast full scan on these indexes will depend on your parameter settings and values for system stats, here are the figures from one system (from autotrace) running 12.1 – the behaviour is consistent across several versions:
select /*+ index_ffs(t1 t1_pk) */ count(*) from t1; select /*+ index_ffs(t1 t1_flag) */ count(*) from t1 where flag is not null; select /*+ index_ffs(t1 t1_ia) */ count(*) from t1; select /*+ index_ffs(t1 t1_ib) */ count(*) from t1; select /*+ index_ffs(t1 t1_ic) */ count(*) from t1; -- with autotrace results: ----------------------------------------------------------------------- | Id | Operation | Name | Rows | Cost (%CPU)| Time | ----------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | 63 (2)| 00:00:01 | | 1 | SORT AGGREGATE | | 1 | | | | 2 | INDEX FAST FULL SCAN| T1_PK | 100K| 63 (2)| 00:00:01 | ----------------------------------------------------------------------- --------------------------------------------------------------------------------- | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time | --------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | 3 | 56 (2)| 00:00:01 | | 1 | SORT AGGREGATE | | 1 | 3 | | | |* 2 | INDEX FAST FULL SCAN| T1_FLAG | 100K| 292K| 56 (2)| 00:00:01 | --------------------------------------------------------------------------------- Predicate Information (identified by operation id): --------------------------------------------------- 2 - filter("FLAG" IS NOT NULL) ----------------------------------------------------------------------- | Id | Operation | Name | Rows | Cost (%CPU)| Time | ----------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | 142 (1)| 00:00:01 | | 1 | SORT AGGREGATE | | 1 | | | | 2 | INDEX FAST FULL SCAN| T1_IA | 100K| 142 (1)| 00:00:01 | ----------------------------------------------------------------------- ----------------------------------------------------------------------- | Id | Operation | Name | Rows | Cost (%CPU)| Time | ----------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | 104 (1)| 00:00:01 | | 1 | SORT AGGREGATE | | 1 | | | | 2 | INDEX FAST FULL SCAN| T1_IB | 100K| 104 (1)| 00:00:01 | ----------------------------------------------------------------------- ----------------------------------------------------------------------- | Id | Operation | Name | Rows | Cost (%CPU)| Time | ----------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | 181 (1)| 00:00:01 | | 1 | SORT AGGREGATE | | 1 | | | | 2 | INDEX FAST FULL SCAN| T1_IC | 100K| 181 (1)| 00:00:01 | -----------------------------------------------------------------------
If you compare the different costs of the fast full scans they’re consistent with the different sizes (leaf_blocks) of the indexes; so you might expect the following query to do either a tablescan or an index join between the t1_flag index and the t1_pk index (which is the smallest candidate index to find the id column):
select sum(id) from t1 where flag = 0 ;
But here’s the plan I got:
-------------------------------------------------------------------------------------------- | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time | -------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | 8 | 528 (1)| 00:00:01 | | 1 | SORT AGGREGATE | | 1 | 8 | | | |* 2 | VIEW | index$_join$_001 | 5000 | 40000 | 528 (1)| 00:00:01 | |* 3 | HASH JOIN | | | | | | |* 4 | INDEX RANGE SCAN | T1_FLAG | 5000 | 40000 | 10 (0)| 00:00:01 | | 5 | INDEX FAST FULL SCAN| T1_IA | 5000 | 40000 | 646 (1)| 00:00:01 | -------------------------------------------------------------------------------------------- Predicate Information (identified by operation id): --------------------------------------------------- 2 - filter("FLAG"=0) 3 - access(ROWID=ROWID) 4 - access("FLAG"=0)
Four things to notice:
- The optimizer has picked the wrong index
- The fast full scan of t1_ia is 646 in this plan when (on its own) it was only 142
- The cost of the whole query is less than the cost of one of the lines
- The index chosen looks as if it might have been selected on the basis of alphabetical order
Oops.
Fortunately, of course, we can always add hints to get the right plan – so let’s try this – and this time the plan is what I got by using explain plan followed by a call to dbms_xplan() with the ‘outline’ option:
explain plan for select /*+ qb_name(main) index_join(@main t1 t1_flag t1_pk) */ sum(id) from t1 where flag = 0 ; select * from table(dbms_xplan.display(null,null,'outline alias')); -------------------------------------------------------------------------------------------- | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time | -------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | 8 | 528 (1)| 00:00:01 | | 1 | SORT AGGREGATE | | 1 | 8 | | | |* 2 | VIEW | index$_join$_001 | 5000 | 40000 | 528 (1)| 00:00:01 | |* 3 | HASH JOIN | | | | | | |* 4 | INDEX RANGE SCAN | T1_FLAG | 5000 | 40000 | 10 (0)| 00:00:01 | | 5 | INDEX FAST FULL SCAN| T1_IA | 5000 | 40000 | 646 (1)| 00:00:01 | -------------------------------------------------------------------------------------------- Query Block Name / Object Alias (identified by operation id): ------------------------------------------------------------- 1 - MAIN 2 - SEL$998059AF / T1@MAIN 3 - SEL$998059AF 4 - SEL$998059AF / indexjoin$_alias$_001@SEL$998059AF 5 - SEL$998059AF / indexjoin$_alias$_002@SEL$998059AF Outline Data ------------- /*+ BEGIN_OUTLINE_DATA INDEX_JOIN(@"MAIN" "T1"@"MAIN" ("T1"."FLAG") ("T1"."ID" "T1"."V20")) OUTLINE(@"MAIN") OUTLINE_LEAF(@"MAIN") OUTLINE_LEAF(@"SEL$998059AF") ALL_ROWS DB_VERSION('12.1.0.1') OPTIMIZER_FEATURES_ENABLE('12.1.0.1') IGNORE_OPTIM_EMBEDDED_HINTS END_OUTLINE_DATA */
Ouch – the optimizer has ignored the hint and is still using the wrong index.
Here’s something really odd, though – and I’ll get around to looking at the 10053 eventually – let’s add an (undocumented) outline_leaf() hint to the query, a hint that is already in the Outline Data:
explain plan for select /*+ qb_name(main) outline_leaf(@main) index_join(@main t1 t1_flag t1_pk) */ sum(id) from t1 where flag = 0 ; select * from table(dbms_xplan.display(null,null,'outline alias')); -------------------------------------------------------------------------------------------- | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time | -------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | 8 | 235 (1)| 00:00:01 | | 1 | SORT AGGREGATE | | 1 | 8 | | | |* 2 | VIEW | index$_join$_001 | 5000 | 40000 | 235 (1)| 00:00:01 | |* 3 | HASH JOIN | | | | | | |* 4 | INDEX RANGE SCAN | T1_FLAG | 5000 | 40000 | 10 (0)| 00:00:01 | | 5 | INDEX FAST FULL SCAN| T1_PK | 5000 | 40000 | 280 (1)| 00:00:01 | -------------------------------------------------------------------------------------------- Query Block Name / Object Alias (identified by operation id): ------------------------------------------------------------- 1 - MAIN 2 - SEL$998059AF / T1@MAIN 3 - SEL$998059AF 4 - SEL$998059AF / indexjoin$_alias$_001@SEL$998059AF 5 - SEL$998059AF / indexjoin$_alias$_002@SEL$998059AF Outline Data ------------- /*+ BEGIN_OUTLINE_DATA INDEX_JOIN(@"MAIN" "T1"@"MAIN" ("T1"."FLAG") ("T1"."ID")) OUTLINE(@"MAIN") OUTLINE_LEAF(@"MAIN") OUTLINE_LEAF(@"SEL$998059AF") ALL_ROWS DB_VERSION('12.1.0.1') OPTIMIZER_FEATURES_ENABLE('12.1.0.1') IGNORE_OPTIM_EMBEDDED_HINTS END_OUTLINE_DATA */ Predicate Information (identified by operation id): --------------------------------------------------- 2 - filter("FLAG"=0) 3 - access(ROWID=ROWID) 4 - access("FLAG"=0)
We get the plan we want, and it’s cheaper than the default one. It does still suffer from the problem that the cost of the fast full scan is larger than it should be (it seems to be the cost of an index fast full scan plus the cost of an index full scan) and the cost of the whole plan is still less than the cost of that one line.
There have been a number of cases where I’ve thought that the optimizer hasn’t chosen an index join when it was a sensible choice – this is probably one of the reasons why.
Update Sept 2018
The problem still reproduces in Oracle 18.1
Update Sept 2019
The problem still reproduces in Oracle 19.3 (and 18.3). Execution plans in Oracle 19 have a “Hint Report” section, and it is interesting to note that when I hint the correct pair of indexes (but don’t include the supposedly redundant outline_leaf() hint) the hint report shows the following:
Hint Report (identified by operation id / Query Block Name / Object Alias): Total hints for statement: 1 (U - Unused (1)) --------------------------------------------------------------------------- 1 - MAIN / T1@MAIN U - index_join(@main t1 (flag) (id))
This tells me that the hint was a valid hint, but has not been used.
However, as often happens when I review a script months or years after writing it, I thought of one more test to run – adding in a hint to tell the optimizer that I didn’t want it to use any indexes apart from the two I was interested in.
/*+ qb_name(main) no_index(@main t1 (id, v10) (id, v20) (id, v30)) index_join(@main t1 (flag) (id)) */
This gave me the index join I wanted to see – even in 11.2.0.4 (the earliest version I retested).
Interesting case. On a side note: 12C seems to have quite a number of inconsistencies in the plan display. Either the cost or the buffers don’t propagate all the way ‘up’ to the top in many of the exec plans I’ve seen. Something is broken :)
Comment by Andy — January 4, 2014 @ 3:47 pm GMT Jan 4,2014 |
Andy,
“Buffers not propagating” – is this from a query hinted with “rowsource_execution_statistics”, or some other method ? Might this be associated in some way with “STATISTICS COLLECTOR” operations and dynamically changing paths ?
The cost (and rows) column has had some glitches ever since NLJ prefetch and batching appeared for nested loops, and odd little consistencies with subqueries have been creeping in since Oracle refined the output to cater for subquery execution estimates.
Do you have any observations of categories of errors, or any interesting (or surprising, perhaps) examples that can be modeled easily ?
Comment by Jonathan Lewis — January 5, 2014 @ 10:27 am GMT Jan 5,2014 |
Yes, it looks like doing by name. I first tried the steps as given by you in your post. The plans all came out to be exactly the same. I did a 10053 trace as well. In there it showed that it just tried one index join option –
Then I changed the name of the PK index from T1_PK to PK_T1 and even though it shows HINT ignored, the plan looks to be what we want.
Comment by vikramrathour — January 6, 2014 @ 5:44 am GMT Jan 6,2014 |
Sorry, forgot to mention that I am using Oracle XE 11gr2
Comment by vikramrathour — January 6, 2014 @ 5:48 am GMT Jan 6,2014 |
Vikramrathour,
Thanks for producing some evidence to support the hypothesis; it’s not the first case where I’ve seen the optimizer “forget” which indexes it’s supposed to use, often in ways that suggest a pointer is not incremented, or a counter starts at 1 when it should have started at zero. This case looks more like a pointer not being set at all when it should have been.
It’s interesting to note that in the original trace Oracle does actually flag the two indexes that have been hinted, before using the wrong one.
The puzzle still remains – why does adding the outline_leaf() hint seem to make a difference ?
Comment by Jonathan Lewis — January 6, 2014 @ 10:32 am GMT Jan 6,2014 |
I have tried looking at the 10053 trace with the OUTLINE_LEAF() hint, and the only thing different (apart from the right index being chosen) is the below line –
which is odd. No idea why these lines are different
Comment by vikramrathour — January 7, 2014 @ 5:56 am GMT Jan 7,2014 |
Hi,
I can’t remember exactly what I used, but it was either the ‘STATISTICS_LEVEL=ALL’ at the session level, or the ‘GATHER_PLAN_STATISTICS’ in the statement hint. The ‘buffer’ I was talking about is the column output from the DBMS_XPLAN.DISPLAY_CURSOR output with either of those two things turned on. Let me dig into my notebook and see if I can find any of those test cases. Recalling from memory, every one of them has something to do with index scan of sort.
Comment by Andy — January 6, 2014 @ 11:49 am GMT Jan 6,2014 |
Andy,
Thanks – if you can find them I’m sure people will want to see them.
Comment by Jonathan Lewis — January 6, 2014 @ 6:06 pm GMT Jan 6,2014 |
Jonathan,
Do you have any idea about an appropriate calculation for the total index join cost?
Comment by Nenad Noveljic — September 23, 2020 @ 9:46 am BST Sep 23,2020 |
Nenad,
Thanks for the question, and sorry for the slow response.
Your question prompted me to have a look at a 10053 trace file from 19c and think about the problem.
First – the basic cost mechanism for the index join is the same as the standard costing method for a hash join, viz:
The 10053 trace file shows the index fast full scan of the second index correctly, and derives the cost of the hash join correctly – then the execution plan introduces a new number (from where I don’t yet know) to report an incorrect cost of the second index fast full scan.
The other thing I found out is that when considering an index join the optimizer seems to stop after testing one index – and it seems to go through the indexes in alphabetical order looking for a suitable “second index” in my example. So if I rename my primary key index t1_i instead of t1_pk, it will be used instead of t1_ia in the case where I don’t specify any indexes in the index_join() hint.
Regards
Jonathan Lewis
Comment by Jonathan Lewis — September 29, 2020 @ 11:41 am BST Sep 29,2020 |
[…] recent comment made a few days on a blog about the optimizer’s “index-join” access path reminded me that I had a few notes to finish and publish that might help some people […]
Pingback by Index FFS Cost | Oracle Scratchpad — October 1, 2020 @ 11:45 am BST Oct 1,2020 |