Oracle SQL
  • LICENSE

Inlist iterator again

Posted on February 21, 2014 by Sayan Malakshinov Posted in curious, oracle, query optimizing 3,111 Page views

Several months ago I wrote about avoiding inlist iterator, but this post about how to force inlist iterator in queries like that:

select *
from xt_test1 t1
    ,xt_test2 t2
where 
     t1.a = :a 
 and t2.id in (a,b)

i.e. when we need to get rows from big table using index by list of values from another table.

Test tables:

ddl

[sourcecode language=”sql”]
drop table xt_test1 purge;
drop table xt_test2 purge;

create table xt_test1(id, a not null, b not null, constraint xt_test1_pk primary key (id))
as
select level, level*2-1, level*2
from dual
connect by level <=10000
/

create table xt_test2(id, pad,constraint xt_test2_pk primary key(id))
as
select level,lpad(level,20)
from dual
connect by level <=10000
/
begin
dbms_stats.gather_table_stats(user,’XT_TEST1′);
dbms_stats.gather_table_stats(user,’XT_TEST2′);
end;
/
[/sourcecode]

[collapse]

By default we can get only bad plan with concatenation and 2 full table scans for this query:

Plan hash value: 667870028

---------------------------------------------------------------------------------------------
| Id  | Operation                     | Name        | Rows  | Bytes | Cost (%CPU)| Time     |
---------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT              |             |     2 |    74 |    22   (0)| 00:00:01 |
|   1 |  CONCATENATION                |             |       |       |            |          |
|   2 |   NESTED LOOPS                |             |     1 |    37 |    11   (0)| 00:00:01 |
|   3 |    NESTED LOOPS               |             |     1 |    37 |    11   (0)| 00:00:01 |
|*  4 |     TABLE ACCESS FULL         | XT_TEST1    |     1 |    12 |    10   (0)| 00:00:01 |
|*  5 |     INDEX UNIQUE SCAN         | XT_TEST2_PK |     1 |       |     0   (0)| 00:00:01 |
|   6 |    TABLE ACCESS BY INDEX ROWID| XT_TEST2    |     1 |    25 |     1   (0)| 00:00:01 |
|   7 |   NESTED LOOPS                |             |     1 |    37 |    11   (0)| 00:00:01 |
|   8 |    NESTED LOOPS               |             |     1 |    37 |    11   (0)| 00:00:01 |
|*  9 |     TABLE ACCESS FULL         | XT_TEST1    |     1 |    12 |    10   (0)| 00:00:01 |
|* 10 |     INDEX UNIQUE SCAN         | XT_TEST2_PK |     1 |       |     0   (0)| 00:00:01 |
|  11 |    TABLE ACCESS BY INDEX ROWID| XT_TEST2    |     1 |    25 |     1   (0)| 00:00:01 |
---------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   4 - filter("T1"."A"=TO_NUMBER(:A))
   5 - access("T2"."ID"="B")
   9 - filter("T1"."A"=TO_NUMBER(:A))
  10 - access("T2"."ID"="A")
       filter(LNNVL("T2"."ID"="B"))

27 rows selected.

I have tried several different options and found one interesting thing:

SQL> explain plan for
  2  select *
  3  from xt_test1 t1
  4      ,xt_test2 t2
  5  where
  6       t1.a = :a
  7   and t2.rowid = (select x.rowid rid from xt_test2 x where x.id in (t1.a,t1.b))
  8  /

Explained.

SQL> @xplan

PLAN_TABLE_OUTPUT
-------------------------------------------------------------------------------------------
Plan hash value: 951366071

-------------------------------------------------------------------------------------------
| Id  | Operation                   | Name        | Rows  | Bytes | Cost (%CPU)| Time     |
-------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT            |             |     1 |    37 | 14061   (1)| 00:02:49 |
|   1 |  NESTED LOOPS               |             | 10000 |   361K|    11   (0)| 00:00:01 |
|*  2 |   TABLE ACCESS FULL         | XT_TEST1    |     1 |    12 |    10   (0)| 00:00:01 |
|   3 |   TABLE ACCESS BY USER ROWID| XT_TEST2    | 10000 |   244K|     1   (0)| 00:00:01 |
|   4 |    INLIST ITERATOR          |             |       |       |            |          |
|*  5 |     INDEX UNIQUE SCAN       | XT_TEST2_PK |     2 |    32 |     2   (0)| 00:00:01 |
-------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   2 - filter("T1"."A"=TO_NUMBER(:A))
   5 - access("X"."ID"=:B1 OR "X"."ID"=:B2)

As you can see it’s very good plan, but only for cases when you have one row only for any set of (t1.a, t2.b).
Look what will be if we replace “=” to “in”:
(strictly speaking we have to use “in (select distinct rowid“, but it’s not so important now)

SQL> explain plan for
  2  select *
  3  from xt_test1 t1
  4      ,xt_test2 t2
  5  where
  6       t1.a = :a
  7   and t2.rowid in (select x.rowid rid from xt_test2 x where x.id in (t1.a,t1.b))
  8  /

---------------------------------------------------------------------------------------------
| Id  | Operation                     | Name        | Rows  | Bytes | Cost (%CPU)| Time     |
---------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT              |             |     2 |    74 |    22   (0)| 00:00:01 |
|   1 |  CONCATENATION                |             |       |       |            |          |
|   2 |   NESTED LOOPS                |             |     1 |    37 |    11   (0)| 00:00:01 |
|   3 |    NESTED LOOPS               |             |     1 |    37 |    11   (0)| 00:00:01 |
|*  4 |     TABLE ACCESS FULL         | XT_TEST1    |     1 |    12 |    10   (0)| 00:00:01 |
|*  5 |     INDEX UNIQUE SCAN         | XT_TEST2_PK |     1 |       |     0   (0)| 00:00:01 |
|   6 |    TABLE ACCESS BY INDEX ROWID| XT_TEST2    |     1 |    25 |     1   (0)| 00:00:01 |
|   7 |   NESTED LOOPS                |             |     1 |    37 |    11   (0)| 00:00:01 |
|   8 |    NESTED LOOPS               |             |     1 |    37 |    11   (0)| 00:00:01 |
|*  9 |     TABLE ACCESS FULL         | XT_TEST1    |     1 |    12 |    10   (0)| 00:00:01 |
|* 10 |     INDEX UNIQUE SCAN         | XT_TEST2_PK |     1 |       |     0   (0)| 00:00:01 |
|  11 |    TABLE ACCESS BY INDEX ROWID| XT_TEST2    |     1 |    25 |     1   (0)| 00:00:01 |
---------------------------------------------------------------------------------------------

Query Block Name / Object Alias (identified by operation id):
-------------------------------------------------------------

   1 - SEL$62C663B8
   4 - SEL$62C663B8_1 / T1@SEL$1
   5 - SEL$62C663B8_1 / X@SEL$2
   6 - SEL$62C663B8_1 / X@SEL$2
   9 - SEL$62C663B8_2 / T1@SEL$62C663B8_2
  10 - SEL$62C663B8_2 / X@SEL$62C663B8_2
  11 - SEL$62C663B8_2 / X@SEL$62C663B8_2

Predicate Information (identified by operation id):
---------------------------------------------------

   4 - filter("T1"."A"=TO_NUMBER(:A))
   5 - access("X"."ID"="T1"."B")
   9 - filter("T1"."A"=TO_NUMBER(:A))
  10 - access("X"."ID"="T1"."A")
       filter(LNNVL("X"."ID"="T1"."B"))

Notice that join with “t2” was eliminated(also we can get join elimination of X).
So let’s try with outlines from plan with “=”

SQL> explain plan for
  2  select
  3    /*+
  4        BEGIN_OUTLINE_DATA
  5        INDEX(@"SEL$2" "X"@"SEL$2" ("XT_TEST2"."ID"))
  6        PUSH_SUBQ(@"SEL$2")
  7        USE_NL(@"SEL$1" "T2"@"SEL$1")
  8        LEADING(@"SEL$1" "T1"@"SEL$1" "T2"@"SEL$1")
  9        ROWID(@"SEL$1" "T2"@"SEL$1")
 10        FULL(@"SEL$1" "T1"@"SEL$1")
 11        OUTLINE_LEAF(@"SEL$1")
 12        OUTLINE_LEAF(@"SEL$2")
 13        ALL_ROWS
 14        DB_VERSION('11.2.0.4')
 15        OPTIMIZER_FEATURES_ENABLE('11.2.0.4')
 16        IGNORE_OPTIM_EMBEDDED_HINTS
 17        END_OUTLINE_DATA
 18    */
 19     *
 20  from xt_test1 t1
 21      ,xt_test2 t2
 22  where
 23       t1.a = :a
 24   and t2.rowid in (select x.rowid rid from xt_test2 x where x.id in (t1.a,t1.b))
 25  /

Explained.

SQL> @xplan +alias

PLAN_TABLE_OUTPUT
------------------------------------------------------------------------------------
Plan hash value: 3405631984

------------------------------------------------------------------------------------
| Id  | Operation            | Name        | Rows  | Bytes | Cost (%CPU)| Time     |
------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT     |             |   500 | 18500 |    27   (4)| 00:00:01 |
|   1 |  NESTED LOOPS        |             |   500 | 18500 |    24   (0)| 00:00:01 |
|*  2 |   TABLE ACCESS FULL  | XT_TEST1    |     1 |    12 |    10   (0)| 00:00:01 |
|*  3 |   TABLE ACCESS FULL  | XT_TEST2    |   500 | 12500 |    14   (0)| 00:00:01 |
|   4 |    INLIST ITERATOR   |             |       |       |            |          |
|*  5 |     INDEX UNIQUE SCAN| XT_TEST2_PK |     1 |    16 |     2   (0)| 00:00:01 |
------------------------------------------------------------------------------------

Query Block Name / Object Alias (identified by operation id):
-------------------------------------------------------------

   1 - SEL$1
   2 - SEL$1 / T1@SEL$1
   3 - SEL$1 / T2@SEL$1
   4 - SEL$2
   5 - SEL$2 / X@SEL$2

Predicate Information (identified by operation id):
---------------------------------------------------

   2 - filter("T1"."A"=TO_NUMBER(:A))
   3 - filter( EXISTS (SELECT /*+ PUSH_SUBQ INDEX ("X" "XT_TEST2_PK") */ 0
              FROM "XT_TEST2" "X" WHERE ("X"."ID"=:B1 OR "X"."ID"=:B2) AND "X".ROWID=:B3))
   5 - access("X"."ID"=:B1 OR "X"."ID"=:B2)
       filter("X".ROWID=:B1)

Unfortunately “TABLE ACCESS BY USER ROWID” was replaced by “TABLE ACCESS FULL”.
I will try to register SR later about this strange behavior, but I want to show now a couple workarounds for that.
First of all we can use collection iterators:

SQL> explain plan for
  2  select/*+ use_nl(t2) index(t2 (id)) cardinality(x 2)*/ *
  3  from xt_test1 t1
  4      ,table(sys.odcinumberlist(a,b)) x
  5      ,xt_test2 t2
  6  where
  7       t1.a = :a
  8   and t2.id=x.column_value
  9  /

PLAN_TABLE_OUTPUT
--------------------------------------------------------------------------------------------
Plan hash value: 651710375

---------------------------------------------------------------------------------------------
| Id  | Operation                                | Name        | Rows  | Bytes | Cost (%CPU)|
---------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                         |             |     2 |    78 |    41   (0)|
|   1 |  NESTED LOOPS                            |             |     2 |    78 |    41   (0)|
|   2 |   NESTED LOOPS                           |             |     2 |    78 |    41   (0)|
|   3 |    NESTED LOOPS                          |             |     2 |    28 |    39   (0)|
|   4 |     TABLE ACCESS FULL                    | XT_TEST1    |     1 |    12 |    10   (0)|
|   5 |     COLLECTION ITERATOR CONSTRUCTOR FETCH|             |     2 |     4 |    29   (0)|
|   6 |    INDEX UNIQUE SCAN                     | XT_TEST2_PK |     1 |       |     0   (0)|
|   7 |   TABLE ACCESS BY INDEX ROWID            | XT_TEST2    |     1 |    25 |     1   (0)|
---------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   4 - filter("T1"."A"=TO_NUMBER(:A))
   6 - access("T2"."ID"=VALUE(KOKBF$))

It’s quite good solution, but there is a big overhead because of many objects creations.
So let’s try another options:

With generated tables

[sourcecode language=”sql”]
SQL> explain plan for
2 with gen as (select level rn from dual connect by level<=2)
3 select/*+ leading(t1 gen t2) use_nl(gen) */ *
4 from xt_test1 t1
5 ,gen
6 ,xt_test2 t2
7 where
8 t1.a = :a
9 and t2.id=decode(gen.rn,1,t1.a,t1.b)
10 /

Explained.

————————————————————————————-
Plan hash value: 2353516195

————————————————————————————-
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)|
————————————————————————————-
| 0 | SELECT STATEMENT | | 1 | 50 | 13 (0)|
| 1 | NESTED LOOPS | | 1 | 50 | 13 (0)|
| 2 | NESTED LOOPS | | 1 | 50 | 13 (0)|
| 3 | NESTED LOOPS | | 1 | 25 | 12 (0)|
|* 4 | TABLE ACCESS FULL | XT_TEST1 | 1 | 12 | 10 (0)|
| 5 | VIEW | | 1 | 13 | 2 (0)|
|* 6 | CONNECT BY WITHOUT FILTERING| | | | |
| 7 | FAST DUAL | | 1 | | 2 (0)|
|* 8 | INDEX UNIQUE SCAN | XT_TEST2_PK | 1 | | 0 (0)|
| 9 | TABLE ACCESS BY INDEX ROWID | XT_TEST2 | 1 | 25 | 1 (0)|
————————————————————————————-

Predicate Information (identified by operation id):
—————————————————

4 – filter("T1"."A"=TO_NUMBER(:A))
6 – filter(LEVEL<=2)
8 – access("T2"."ID"=DECODE("GEN"."RN",1,"T1"."A","T1"."B"))
[/sourcecode]

[collapse]
Note that you have to avoid duplicates if t1.a = t2.b.
Also it’s quite good to get plans with concatenation only on second table, without rereading others tables:
12c Lateral with concatenation

[sourcecode language=”sql”]
SQL> explain plan for
2 select/*+ leading(t1 t2) use_nl(t2) */ *
3 from xt_test1 t1
4 ,lateral(
5 select/*+ no_merge */ *
6 from xt_test2
7 where xt_test2.id in (t1.a,t1.b)
8 ) t2
9 where
10 t1.a = :a
11 /

PLAN_TABLE_OUTPUT
—————————————————————————————
Plan hash value: 53936152

—————————————————————————————
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)|
—————————————————————————————
| 0 | SELECT STATEMENT | | 2 | 134 | 14 (0)|
| 1 | NESTED LOOPS | | 2 | 134 | 14 (0)|
|* 2 | TABLE ACCESS FULL | XT_TEST1 | 1 | 12 | 10 (0)|
| 3 | VIEW | VW_LAT_A18161FF | 2 | 110 | 4 (0)|
| 4 | CONCATENATION | | | | |
| 5 | TABLE ACCESS BY INDEX ROWID| XT_TEST2 | 1 | 25 | 2 (0)|
|* 6 | INDEX UNIQUE SCAN | XT_TEST2_PK | 1 | | 1 (0)|
| 7 | TABLE ACCESS BY INDEX ROWID| XT_TEST2 | 1 | 25 | 2 (0)|
|* 8 | INDEX UNIQUE SCAN | XT_TEST2_PK | 1 | | 1 (0)|
—————————————————————————————

Predicate Information (identified by operation id):
—————————————————

2 – filter("T1"."A"=TO_NUMBER(:A))
6 – access("XT_TEST2"."ID"="T1"."B")
8 – access("XT_TEST2"."ID"="T1"."A")
filter(LNNVL("XT_TEST2"."ID"="T1"."B"))
[/sourcecode]

[collapse]

But it’s really funny, that do the same with simple ANSI LEFT OUTER join!
Ansi left join with concatenation

[sourcecode language=”sql” highlight=”7″]
SQL> explain plan for
2 select/*+ leading(t1 t2) use_nl(t2) */ *
3 from xt_test1 t1
4 left join xt_test2 t2
5 on t2.id in (t1.a,t1.b)
6 where t1.a = :a
7 and t2.id*0 is not null
8 /
———————————————————————————————–
Plan hash value: 4273891864

———————————————————————————————–
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
———————————————————————————————–
| 0 | SELECT STATEMENT | | 2 | 134 | 14 (0)| 00:00:01 |
|* 1 | FILTER | | | | | |
| 2 | NESTED LOOPS OUTER | | 2 | 134 | 14 (0)| 00:00:01 |
|* 3 | TABLE ACCESS FULL | XT_TEST1 | 1 | 12 | 10 (0)| 00:00:01 |
| 4 | VIEW | | 2 | 110 | 4 (0)| 00:00:01 |
| 5 | CONCATENATION | | | | | |
| 6 | TABLE ACCESS BY INDEX ROWID| XT_TEST2 | 1 | 25 | 2 (0)| 00:00:01 |
|* 7 | INDEX UNIQUE SCAN | XT_TEST2_PK | 1 | | 1 (0)| 00:00:01 |
| 8 | TABLE ACCESS BY INDEX ROWID| XT_TEST2 | 1 | 25 | 2 (0)| 00:00:01 |
|* 9 | INDEX UNIQUE SCAN | XT_TEST2_PK | 1 | | 1 (0)| 00:00:01 |
———————————————————————————————–

Predicate Information (identified by operation id):
—————————————————

1 – filter("T2"."ID"*0 IS NOT NULL)
3 – filter("T1"."A"=TO_NUMBER(:A))
7 – access("T2"."ID"="T1"."B")
9 – access("T2"."ID"="T1"."A")
filter(LNNVL("T2"."ID"="T1"."B"))

[/sourcecode]

[collapse]

inlist iterator
« Oracle 12c: scalar subqueries
Friday prank: select from join join join »
Page views: 3,111
photo Sayan Malakshinov

Oracle ACE Pro Oracle ACE Pro Alumni

DEVVYOracle Database Developer Choice Award winner

Oracle performance tuning expert

UK / Cambridge

LinkedIn   Twitter
sayan@orasql.org

Recent Posts

  • Oracle Telegram Bot
  • Partition Pruning and Global Indexes
  • Interval Search: Part 4. Dynamic Range Segmentation – interval quantization
  • Interval Search Series: Simplified, Advanced, and Custom Solutions
  • Interval Search: Part 3. Dynamic Range Segmentation – Custom Domain Index

Popular posts

Recent Comments

  • Oracle SQL | Interval Search: Part 4. Dynamic Range Segmentation – interval quantization on Interval Search: Part 3. Dynamic Range Segmentation – Custom Domain Index
  • Oracle SQL | Interval Search: Part 4. Dynamic Range Segmentation – interval quantization on Interval Search: Part 2. Dynamic Range Segmentation – Simplified
  • Oracle SQL | Interval Search: Part 4. Dynamic Range Segmentation – interval quantization on Interval Search: Optimizing Date Range Queries – Part 1
  • Oracle SQL | Interval Search Series: Simplified, Advanced, and Custom Solutions on Interval Search: Part 2. Dynamic Range Segmentation – Simplified
  • Oracle SQL | Interval Search: Part 2. Dynamic Range Segmentation – Simplified on Interval Search: Part 3. Dynamic Range Segmentation – Custom Domain Index

Blogroll

  • Alex Fatkulin
  • Alexander Anokhin
  • Andrey Nikolaev
  • Charles Hooper
  • Christian Antognini
  • Coskan Gundogar
  • David Fitzjarrell
  • Igor Usoltsev
  • Jonathan Lewis
  • Karl Arao
  • Mark Bobak
  • Martin Bach
  • Martin Berger
  • Neil Chandler
  • Randolf Geist
  • Richard Foote
  • Riyaj Shamsudeen
  • Tanel Poder
  • Timur Akhmadeev
  • Valentin Nikotin
©Sayan Malakshinov. Oracle SQL