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:
[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]
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:
[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]
Also it’s quite good to get plans with concatenation only on second table, without rereading others tables:
[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]
But it’s really funny, that do the same with simple ANSI LEFT OUTER join!
[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]
