Sunday, January 11, 2026

Oracle SQL MEMBER vs. IN Comparison Condition Performance - Part2


(I)-Oracle SQL MEMBER vs. IN Comparison Condition Performance - Part1      (II)-Oracle SQL MEMBER vs. IN Comparison Condition Performance - Part2


Continuing with last Blog: (I)-Oracle SQL MEMBER vs. IN Comparison Condition Performance - Part1,
in this Blog, we will demonstrate the performance difference of MEMBER and IN Comparison Conditions for nested table column,
and performance improvement by creating index on nested child table.

Note: Tested in Oracle 19.27


1. MEMBER vs. IN Performance Difference on Nested Table Column


We create a table containing one nested table column, and run tests with MEMBER vs. IN Conditions.

drop table test_table;

create table test_table (id number, nt t_num_tab) nested table nt store as test_table_nt;

insert into test_table(id, nt) select level, t_num_tab(level, -level) from dual connect by level <= 1e5; 

commit; 

begin
   dbms_stats.gather_table_stats(user, 'TEST_TABLE',    cascade=>true);
   dbms_stats.gather_table_stats(user, 'TEST_TABLE_NT', cascade=>true);
end;
/

select table_name, num_rows, blocks, avg_row_len from dba_tables where table_name in ('TEST_TABLE', 'TEST_TABLE_NT');
  TABLE_NAME        NUM_ROWS     BLOCKS AVG_ROW_LEN
  --------------- ---------- ---------- -----------
  TEST_TABLE          100000        496          22
  TEST_TABLE_NT       200000        874          22
 
select table_name, index_name, uniqueness, leaf_blocks, num_rows, distinct_keys from dba_indexes where table_name in ('TEST_TABLE', 'TEST_TABLE_NT');
  TABLE_NAME      INDEX_NAME                UNIQUENESS   LEAF_BLOCKS   NUM_ROWS DISTINCT_KEYS
  --------------- ------------------------- ------------ ----------- ---------- -------------
  TEST_TABLE      SYS_C00144532             UNIQUE               338     100000        100000
  TEST_TABLE_NT   SYS_FK0005992996N00002$   NONUNIQUE            702     200000        102008
 
select segment_name, bytes, blocks from dba_segments where segment_name in ('TEST_TABLE', 'TEST_TABLE_NT', 'SYS_C00142541', 'SYS_FK0005992996N00002$');
  SEGMENT_NAME                   BYTES     BLOCKS
  ------------------------- ---------- ----------
  TEST_TABLE                   4194304        512
  TEST_TABLE_NT                7340032        896
  SYS_C00144532                3145728        384
  SYS_FK0005992996N00002$      6291456        768

create or replace procedure test_TAB_MEMBER_vs_IN_Conditions (p_size number) as
  l_num_tab        t_num_tab := t_num_tab();
  l_start_time     number;
  l_ret_cnt        number;
begin
  select level bulk collect into l_num_tab from dual connect by level <= p_size;
  
  --================= MEMBER Comparison =================
  l_start_time := dbms_utility.get_time;
  
  select /*+ Test_TAB_MEMBER */ count(c.column_value) into l_ret_cnt 
    from test_table t, table(t.nt) c 
   where c.column_value member of cast(l_num_tab as t_num_tab);
   
  dbms_output.put_line('=============== MEMBER Condition ===============');
  dbms_output.put_line('Elapsed(centi)        = '||(dbms_utility.get_time - l_start_time));
  dbms_output.put_line('l_ret_cnt             = '||l_ret_cnt);

  --================= IN Comparison =================
  l_start_time := dbms_utility.get_time;
  
  select /*+ Test_TAB_IN */ count(c.column_value) into l_ret_cnt 
    from test_table t, table(t.nt) c 
   where c.column_value in (select column_value from table(cast(l_num_tab as t_num_tab)));
  
  dbms_output.put_line('=============== IN Condition ===============');
  dbms_output.put_line('Elapsed(centi)        = '||(dbms_utility.get_time - l_start_time));
  dbms_output.put_line('l_ret_cnt             = '||l_ret_cnt);
end;
/
The test result shows that the elapsed time for MEMBER is about 100 times longer than for IN.

SQL > exec test_TAB_MEMBER_vs_IN_Conditions(1000);

  =============== MEMBER Condition ===============
  Elapsed(centi)        = 449
  l_ret_cnt             = 1000
  =============== IN Condition ===============
  Elapsed(centi)        = 3
  l_ret_cnt             = 1000


2. MEMBER vs. IN Performance Difference on Lookup


Blog: MEMBER OF comparison of PL/SQL and SQL (Kim Berg Hansen June 23, 2014) shows the performance difference of single row select in PL/SQL and SQL, and demonstrate SQL performance improvement by creating index on nested child table.

Based on it, we will make tests to show the Performance Difference of lookup on nested table column with and without index on it by MEMBER or IN Comparison Conditions.

Note that this is not applicable to varray array column because it has no storage_table clause,
whereas nested table column has storage_table clause to store it as a normal heap table.


2.1 Single Key Lookup


First we make tests by single key lookup:

alter session set tracefile_identifier = 'nt_single';
alter session set events '10046 trace name context forever, level 12'; 
 				
VARIABLE lookup_key NUMBER;
exec :lookup_key := 999;

------------- No Index ------------
drop index test_table_nt_indx;

select /*+ Test_TAB_MEMBER_LOOKUP_Single_NO_INDX */ count(t.id) from test_table t where :lookup_key member of cast(t.nt as t_num_tab);
select /*+ Test_TAB_IN_LOOKUP_Single_NO_INDX */ count(t.id) from test_table t, table(t.nt) c where c.column_value = :lookup_key;

------------- Index on column_value ------------
drop index test_table_nt_indx;
create index test_table_nt_indx on test_table_nt(column_value);

begin
   dbms_stats.gather_table_stats(user, 'TEST_TABLE',    cascade=>true);
   dbms_stats.gather_table_stats(user, 'TEST_TABLE_NT', cascade=>true);
end;
/

--MEMBER Comparison does not use INDEX 
select /*+ Test_TAB_MEMBER_LOOKUP_Single_INDX_Col_1 */ count(t.id) from test_table t where :lookup_key member of cast(t.nt as t_num_tab);
select /*+ Test_TAB_IN_LOOKUP_Single_INDX_Col_1 */ count(t.id) from test_table t, table(t.nt) c where c.column_value = :lookup_key;

------------- Index on (column_value, nested_table_id) ------------
drop index test_table_nt_indx;
create index test_table_nt_indx on test_table_nt(column_value, nested_table_id);

begin
   dbms_stats.gather_table_stats(user, 'TEST_TABLE',    cascade=>true);
   dbms_stats.gather_table_stats(user, 'TEST_TABLE_NT', cascade=>true);
end;
/

select /*+ Test_TAB_IN_LOOKUP_Single_INDX_Col_2 */ count(t.id) from test_table t, table(t.nt) c where c.column_value = :lookup_key;

alter session set events '10046 trace name context off';

Here the SQL Trace (10046) output:

********************************************************************************
SQL ID: f90kvg63f33fr Plan Hash: 440919791

select /*+ Test_TAB_MEMBER_LOOKUP_Single_NO_INDX */ count(t.id) 
from test_table t where :lookup_key member of cast(t.nt as t_num_tab)

call     count       cpu    elapsed       disk      query    current        rows
------- ------  -------- ---------- ---------- ---------- ----------  ----------
Parse        1      0.00       0.00          0          0          0           0
Execute      1      0.00       0.00          0          0          0           0
Fetch        2      0.35       0.35          0     212122          0           1
------- ------  -------- ---------- ---------- ---------- ----------  ----------
total        4      0.36       0.36          0     212122          0           1

Rows (1st) Row Source Operation
---------- ---------------------------------------------------
         1 SORT AGGREGATE (cr=212122 pr=0 pw=0 time=359533 us starts=1)
         1  FILTER  (cr=212122 pr=0 pw=0 time=359523 us starts=1)
    100000   TABLE ACCESS FULL TEST_TABLE (cr=394 pr=0 pw=0 time=7314 us starts=1 cost=143 size=2200000 card=100000)
    200000   TABLE ACCESS BY INDEX ROWID BATCHED TEST_TABLE_NT (cr=211728 pr=0 pw=0 time=179841 us starts=100000 cost=2 size=44 card=2)
    200000    INDEX RANGE SCAN SYS_FK0005992996N00002$ (cr=111678 pr=0 pw=0 time=100304 us starts=100000 cost=2 size=0 card=2)(object id 5992998)

********************************************************************************
SQL ID: 3b57pxgx3ka1r Plan Hash: 299443699

select /*+ Test_TAB_IN_LOOKUP_Single_NO_INDX */ count(t.id) 
from test_table t, table(t.nt) c where c.column_value = :lookup_key

call     count       cpu    elapsed       disk      query    current        rows
------- ------  -------- ---------- ---------- ---------- ----------  ----------
Parse        1      0.00       0.00          0          0          0           0
Execute      1      0.00       0.00          0          0          0           0
Fetch        2      0.00       0.00          0        792          0           1
------- ------  -------- ---------- ---------- ---------- ----------  ----------
total        4      0.00       0.00          0        792          0           1

Rows (1st) Row Source Operation
---------- ---------------------------------------------------
         1 SORT AGGREGATE (cr=792 pr=0 pw=0 time=4927 us starts=1)
         1  NESTED LOOPS  (cr=792 pr=0 pw=0 time=4919 us starts=1 cost=252 size=44 card=1)
         1   NESTED LOOPS  (cr=791 pr=0 pw=0 time=4910 us starts=1 cost=252 size=44 card=1)
         1    TABLE ACCESS FULL TEST_TABLE_NT (cr=789 pr=0 pw=0 time=4900 us starts=1 cost=251 size=22 card=1)
         1    INDEX UNIQUE SCAN SYS_C00144532 (cr=2 pr=0 pw=0 time=7 us starts=1 cost=1 size=0 card=1)(object id 5992999)
         1   TABLE ACCESS BY INDEX ROWID TEST_TABLE (cr=1 pr=0 pw=0 time=8 us starts=1 cost=1 size=22 card=1)
********************************************************************************

SQL ID: 5pyx85dkay5fz Plan Hash: 440919791

select /*+ Test_TAB_MEMBER_LOOKUP_Single_INDX_Col_1 */ count(t.id) 
from test_table t where :lookup_key member of cast(t.nt as t_num_tab)

call     count       cpu    elapsed       disk      query    current        rows
------- ------  -------- ---------- ---------- ---------- ----------  ----------
Parse        1      0.00       0.00          0          0          0           0
Execute      1      0.00       0.00          0          0          0           0
Fetch        2      0.35       0.35          0     212122          0           1
------- ------  -------- ---------- ---------- ---------- ----------  ----------
total        4      0.35       0.35          0     212122          0           1

Rows (1st) Row Source Operation
---------- ---------------------------------------------------
         1 SORT AGGREGATE (cr=212122 pr=0 pw=0 time=357398 us starts=1)
         1  FILTER  (cr=212122 pr=0 pw=0 time=357388 us starts=1)
    100000   TABLE ACCESS FULL TEST_TABLE (cr=394 pr=0 pw=0 time=8078 us starts=1 cost=143 size=2200000 card=100000)
    200000   TABLE ACCESS BY INDEX ROWID BATCHED TEST_TABLE_NT (cr=211728 pr=0 pw=0 time=178477 us starts=100000 cost=2 size=44 card=2)
    200000    INDEX RANGE SCAN SYS_FK0005992996N00002$ (cr=111678 pr=0 pw=0 time=98691 us starts=100000 cost=2 size=0 card=2)(object id 5992998)
    
********************************************************************************

SQL ID: 77x73ayp09c9p Plan Hash: 1013981520

select /*+ Test_TAB_IN_LOOKUP_Single_INDX_Col_1 */ count(t.id) 
from test_table t, table(t.nt) c where c.column_value = :lookup_key

call     count       cpu    elapsed       disk      query    current        rows
------- ------  -------- ---------- ---------- ---------- ----------  ----------
Parse        1      0.00       0.00          0          0          0           0
Execute      1      0.00       0.00          0          0          0           0
Fetch        2      0.00       0.00          0          6          0           1
------- ------  -------- ---------- ---------- ---------- ----------  ----------
total        4      0.00       0.00          0          6          0           1

Rows (1st) Row Source Operation
---------- ---------------------------------------------------
         1 SORT AGGREGATE (cr=6 pr=0 pw=0 time=40 us starts=1)
         1  NESTED LOOPS  (cr=6 pr=0 pw=0 time=33 us starts=1 cost=2 size=44 card=1)
         1   NESTED LOOPS  (cr=5 pr=0 pw=0 time=26 us starts=1 cost=2 size=44 card=1)
         1    TABLE ACCESS BY INDEX ROWID BATCHED TEST_TABLE_NT (cr=3 pr=0 pw=0 time=18 us starts=1 cost=1 size=22 card=1)
         1     INDEX RANGE SCAN TEST_TABLE_NT_INDX (cr=2 pr=0 pw=0 time=14 us starts=1 cost=1 size=0 card=1)(object id 5993191)
         1    INDEX UNIQUE SCAN SYS_C00144532 (cr=2 pr=0 pw=0 time=6 us starts=1 cost=1 size=0 card=1)(object id 5992999)
         1   TABLE ACCESS BY INDEX ROWID TEST_TABLE (cr=1 pr=0 pw=0 time=5 us starts=1 cost=1 size=22 card=1)
         
********************************************************************************

SQL ID: 535h2a2yb77z0 Plan Hash: 2041402069

select /*+ Test_TAB_IN_LOOKUP_Single_INDX_Col_2 */ count(t.id) 
from test_table t, table(t.nt) c where c.column_value = :lookup_key

call     count       cpu    elapsed       disk      query    current        rows
------- ------  -------- ---------- ---------- ---------- ----------  ----------
Parse        1      0.00       0.00          0          0          0           0
Execute      1      0.00       0.00          0          0          0           0
Fetch        2      0.00       0.00          0          6          0           1
------- ------  -------- ---------- ---------- ---------- ----------  ----------
total        4      0.00       0.00          0          6          0           1

Rows (1st) Row Source Operation
---------- ---------------------------------------------------
         1 SORT AGGREGATE (cr=6 pr=0 pw=0 time=32 us starts=1)
         1  NESTED LOOPS  (cr=6 pr=0 pw=0 time=25 us starts=1 cost=3 size=44 card=1)
         1   NESTED LOOPS  (cr=5 pr=0 pw=0 time=18 us starts=1 cost=3 size=44 card=1)
         1    INDEX RANGE SCAN TEST_TABLE_NT_INDX (cr=3 pr=0 pw=0 time=12 us starts=1 cost=2 size=22 card=1)(object id 5993192)
         1    INDEX UNIQUE SCAN SYS_C00144532 (cr=2 pr=0 pw=0 time=5 us starts=1 cost=1 size=0 card=1)(object id 5992999)
         1   TABLE ACCESS BY INDEX ROWID TEST_TABLE (cr=1 pr=0 pw=0 time=6 us starts=1 cost=1 size=22 card=1)    
         
Above test output shows the MEMBER Comparisons make 212122 block gets (query=212122), mostly on TEST_TABLE_NT and its foreign key index: SYS_FK0005992996N00002$ although both together has only about 1600 blocks.

Whereas IN Comparisons has query=792 when no index, and query=6 when there is the index on nested table column.


2.2 Multi Key Lookup


Now we run tests of lookup with multiple keys:

alter session set tracefile_identifier = 'nt_multi';
alter session set events '10046 trace name context forever, level 12'; 
 				
drop table test_keys; 
create table test_keys as select level k from dual connect by level <= 100; 
begin
   dbms_stats.gather_table_stats(user, 'TEST_KEYS',    cascade=>true);
end;
/

------------- No Index ------------
drop index test_table_nt_indx;

select /*+ Test_TAB_MEMBER_LOOKUP_Multi_NO_INDX */ count(t.id) from test_table t, test_keys l where l.k member of cast(t.nt as t_num_tab);
select /*+ Test_TAB_IN_LOOKUP_Multi_NO_INDX */ count(t.id) from test_table t, table(t.nt) c, test_keys l where c.column_value = l.k;

------------- Index on column_value ------------
drop index test_table_nt_indx;
create index test_table_nt_indx on test_table_nt(column_value);

begin
   dbms_stats.gather_table_stats(user, 'TEST_TABLE',    cascade=>true);
   dbms_stats.gather_table_stats(user, 'TEST_TABLE_NT', cascade=>true);
end;
/

--MEMBER Comparison does not use INDEX  
select /*+ Test_TAB_MEMBER_LOOKUP_Multi_INDX_Col_1 */ count(t.id) from test_table t, test_keys l where l.k member of cast(t.nt as t_num_tab);
select /*+ Test_TAB_IN_LOOKUP_Multi_INDX_Col_1 */ count(t.id) from test_table t, table(t.nt) c, test_keys l where c.column_value = l.k;

------------- Index on (column_value, nested_table_id) ------------
drop index test_table_nt_indx;
create index test_table_nt_indx on test_table_nt(column_value, nested_table_id);

begin
   dbms_stats.gather_table_stats(user, 'TEST_TABLE',    cascade=>true);
   dbms_stats.gather_table_stats(user, 'TEST_TABLE_NT', cascade=>true);
end;
/

select /*+ Test_TAB_IN_LOOKUP_Multi_INDX_Col_2 */ count(t.id) from test_table t, table(t.nt) c, test_keys l where c.column_value = l.k;

alter session set events '10046 trace name context off';

Here SQL Trace (10046) output:

********************************************************************************

SQL ID: a9wc0w7m86gr3 Plan Hash: 2034372327

select /*+ Test_TAB_MEMBER_LOOKUP_Multi_NO_INDX */ count(t.id) 
from test_table t, test_keys l where l.k member of cast(t.nt as t_num_tab)

call     count       cpu    elapsed       disk      query    current        rows
------- ------  -------- ---------- ---------- ---------- ----------  ----------
Parse        1      0.00       0.00          0          0          0           0
Execute      1      0.00       0.00          0          0          0           0
Fetch        2     36.91      37.14          0   21173295          0           1
------- ------  -------- ---------- ---------- ---------- ----------  ----------
total        4     36.91      37.15          0   21173295          0           1

Rows (1st) Row Source Operation
---------- ---------------------------------------------------
         1 SORT AGGREGATE (cr=21173295 pr=0 pw=0 time=37148408 us starts=1)
       100  FILTER  (cr=21173295 pr=0 pw=0 time=36949905 us starts=1)
  10000000   MERGE JOIN CARTESIAN (cr=396 pr=0 pw=0 time=2089627 us starts=1 cost=14177 size=250000000 card=10000000)
       100    TABLE ACCESS FULL TEST_KEYS (cr=2 pr=0 pw=0 time=126 us starts=1 cost=3 size=300 card=100)
  10000000    BUFFER SORT (cr=394 pr=0 pw=0 time=1116011 us starts=100 cost=14174 size=2200000 card=100000)
    100000     TABLE ACCESS FULL TEST_TABLE (cr=394 pr=0 pw=0 time=8323 us starts=1 cost=142 size=2200000 card=100000)
  20000000   TABLE ACCESS BY INDEX ROWID BATCHED TEST_TABLE_NT (cr=21172899 pr=0 pw=0 time=18230528 us starts=10000000 cost=2 size=44 card=2)
  20000000    INDEX RANGE SCAN SYS_FK0005992996N00002$ (cr=11167899 pr=0 pw=0 time=10198247 us starts=10000000 cost=2 size=0 card=2)(object id 5992998)
********************************************************************************

SQL ID: ftr39jncgyj2r Plan Hash: 768666713

select /*+ Test_TAB_IN_LOOKUP_Multi_NO_INDX */ count(t.id) 
from test_table t, table(t.nt) c, test_keys l where c.column_value = l.k

call     count       cpu    elapsed       disk      query    current        rows
------- ------  -------- ---------- ---------- ---------- ----------  ----------
Parse        1      0.00       0.00          0          0          0           0
Execute      1      0.00       0.00          0          0          0           0
Fetch        2      0.02       0.02          0        900          0           1
------- ------  -------- ---------- ---------- ---------- ----------  ----------
total        4      0.02       0.02          0        900          0           1

Rows (1st) Row Source Operation
---------- ---------------------------------------------------
         1 SORT AGGREGATE (cr=900 pr=0 pw=0 time=20282 us starts=1)
       100  NESTED LOOPS  (cr=900 pr=0 pw=0 time=787 us starts=1 cost=304 size=4700 card=100)
       100   NESTED LOOPS  (cr=800 pr=0 pw=0 time=583 us starts=1 cost=304 size=4700 card=100)
       100    HASH JOIN  (cr=791 pr=0 pw=0 time=375 us starts=1 cost=254 size=2500 card=100)
       100     TABLE ACCESS FULL TEST_KEYS (cr=2 pr=0 pw=0 time=45 us starts=1 cost=3 size=300 card=100)
    200000     TABLE ACCESS FULL TEST_TABLE_NT (cr=789 pr=0 pw=0 time=23571 us starts=1 cost=251 size=4400000 card=200000)
       100    INDEX UNIQUE SCAN SYS_C00144532 (cr=9 pr=0 pw=0 time=47 us starts=100 cost=1 size=0 card=1)(object id 5992999)
       100   TABLE ACCESS BY INDEX ROWID TEST_TABLE (cr=100 pr=0 pw=0 time=50 us starts=100 cost=1 size=22 card=1)
********************************************************************************

SQL ID: gqxbmzjcrt5p1 Plan Hash: 2034372327

select /*+ Test_TAB_MEMBER_LOOKUP_Multi_INDX_Col_1 */ count(t.id) 
from test_table t, test_keys l where l.k member of cast(t.nt as t_num_tab)

call     count       cpu    elapsed       disk      query    current        rows
------- ------  -------- ---------- ---------- ---------- ----------  ----------
Parse        1      0.00       0.00          0          0          0           0
Execute      1      0.00       0.00          0          0          0           0
Fetch        2     36.68      36.92          0   21173295          0           1
------- ------  -------- ---------- ---------- ---------- ----------  ----------
total        4     36.69      36.92          0   21173295          0           1

Rows (1st) Row Source Operation
---------- ---------------------------------------------------
         1 SORT AGGREGATE (cr=21173295 pr=0 pw=0 time=36926973 us starts=1)
       100  FILTER  (cr=21173295 pr=0 pw=0 time=35871619 us starts=1)
  10000000   MERGE JOIN CARTESIAN (cr=396 pr=0 pw=0 time=2062528 us starts=1 cost=14177 size=250000000 card=10000000)
       100    TABLE ACCESS FULL TEST_KEYS (cr=2 pr=0 pw=0 time=130 us starts=1 cost=3 size=300 card=100)
  10000000    BUFFER SORT (cr=394 pr=0 pw=0 time=1081777 us starts=100 cost=14174 size=2200000 card=100000)
    100000     TABLE ACCESS FULL TEST_TABLE (cr=394 pr=0 pw=0 time=7557 us starts=1 cost=142 size=2200000 card=100000)
  20000000   TABLE ACCESS BY INDEX ROWID BATCHED TEST_TABLE_NT (cr=21172899 pr=0 pw=0 time=18120678 us starts=10000000 cost=2 size=44 card=2)
  20000000    INDEX RANGE SCAN SYS_FK0005992996N00002$ (cr=11167899 pr=0 pw=0 time=10102918 us starts=10000000 cost=2 size=0 card=2)(object id 5992998)
********************************************************************************

SQL ID: 3hww8x82t497g Plan Hash: 3838096326

select /*+ Test_TAB_IN_LOOKUP_Multi_INDX_Col_1 */ count(t.id) 
from test_table t, table(t.nt) c, test_keys l where c.column_value = l.k

call     count       cpu    elapsed       disk      query    current        rows
------- ------  -------- ---------- ---------- ---------- ----------  ----------
Parse        1      0.00       0.00          0          0          0           0
Execute      1      0.00       0.00          0          0          0           0
Fetch        2      0.00       0.00          0        120          0           1
------- ------  -------- ---------- ---------- ---------- ----------  ----------
total        4      0.00       0.00          0        120          0           1

Rows (1st) Row Source Operation
---------- ---------------------------------------------------
         1 SORT AGGREGATE (cr=120 pr=0 pw=0 time=336 us starts=1)
       100  NESTED LOOPS  (cr=120 pr=0 pw=0 time=1052 us starts=1 cost=103 size=4700 card=100)
       100   NESTED LOOPS  (cr=20 pr=0 pw=0 time=849 us starts=1 cost=103 size=4700 card=100)
       100    NESTED LOOPS  (cr=11 pr=0 pw=0 time=544 us starts=1 cost=53 size=2500 card=100)
       100     TABLE ACCESS FULL TEST_KEYS (cr=2 pr=0 pw=0 time=132 us starts=1 cost=3 size=300 card=100)
       100     TABLE ACCESS BY INDEX ROWID BATCHED TEST_TABLE_NT (cr=9 pr=0 pw=0 time=115 us starts=100 cost=1 size=22 card=1)
       100      INDEX RANGE SCAN TEST_TABLE_NT_INDX (cr=8 pr=0 pw=0 time=74 us starts=100 cost=1 size=0 card=1)(object id 5993194)
       100    INDEX UNIQUE SCAN SYS_C00144532 (cr=9 pr=0 pw=0 time=46 us starts=100 cost=1 size=0 card=1)(object id 5992999)
       100   TABLE ACCESS BY INDEX ROWID TEST_TABLE (cr=100 pr=0 pw=0 time=48 us starts=100 cost=1 size=22 card=1)
********************************************************************************

SQL ID: 5ghun4988xh7w Plan Hash: 2752258125

select /*+ Test_TAB_IN_LOOKUP_Multi_INDX_Col_2 */ count(t.id) 
from test_table t, table(t.nt) c, test_keys l where c.column_value = l.k

call     count       cpu    elapsed       disk      query    current        rows
------- ------  -------- ---------- ---------- ---------- ----------  ----------
Parse        1      0.00       0.00          0          0          0           0
Execute      1      0.00       0.00          0          0          0           0
Fetch        2      0.00       0.00          0        136          0           1
------- ------  -------- ---------- ---------- ---------- ----------  ----------
total        4      0.00       0.00          0        136          0           1

Rows (1st) Row Source Operation
---------- ---------------------------------------------------
         1 SORT AGGREGATE (cr=136 pr=0 pw=0 time=275 us starts=1)
       100  NESTED LOOPS  (cr=136 pr=0 pw=0 time=837 us starts=1 cost=103 size=4700 card=100)
       100   NESTED LOOPS  (cr=36 pr=0 pw=0 time=831 us starts=1 cost=103 size=4700 card=100)
       100    NESTED LOOPS  (cr=27 pr=0 pw=0 time=528 us starts=1 cost=53 size=2500 card=100)
       100     TABLE ACCESS FULL TEST_KEYS (cr=2 pr=0 pw=0 time=121 us starts=1 cost=3 size=300 card=100)
       100     INDEX RANGE SCAN TEST_TABLE_NT_INDX (cr=25 pr=0 pw=0 time=83 us starts=100 cost=1 size=22 card=1)(object id 5993195)
       100    INDEX UNIQUE SCAN SYS_C00144532 (cr=9 pr=0 pw=0 time=44 us starts=100 cost=1 size=0 card=1)(object id 5992999)
       100   TABLE ACCESS BY INDEX ROWID TEST_TABLE (cr=100 pr=0 pw=0 time=47 us starts=100 cost=1 size=22 card=1)
       
Above test output shows for 100 keys, the MEMBER Comparisons make 21,173,295 block gets (query=21173295), mostly on TEST_TABLE_NT and its foreign key index: SYS_FK0005992996N00002$, which is about 100 times of single key lookup (query=212,122)

Whereas IN Comparisons has query=900 when no index, and query=120/136 when there is the index on nested table column, which are linear to number of lookup keys.

Sqltraces shows that for MEMBER Comparison, it is much slower than IN Comparison (37.15 vs. 0.02 seconds)
because there are much higher block gets (query = 21173295 vs. 900), and index on nested table column is not used.

When there are Index on (column_value) or (column_value, nested_table_id), the performance is further improved (query = 900 vs. 120/136).

For Index on (column_value), index TEST_TABLE_NT_INDX and SYS_C00144532 are used, but still need TABLE ACCESS BY INDEX ROWID BATCHED on TEST_TABLE_NT.

For Index on (column_value, nested_table_id), index TEST_TABLE_NT_INDX and SYS_C00144532 are used, no more TEST_TABLE_NT access.

Here xplan with Predicate Information:

--======================= No Index MEMBER Comparison =======================--
drop index test_table_nt_indx;

select /*+ Test_TAB_MEMBER_LOOKUP_Multi_NO_INDX */ count(t.id) 
from test_table t, test_keys l where l.k member of cast(t.nt as t_num_tab);

Plan hash value: 2034372327
-----------------------------------------------------------------------------------------------------------------
| Id  | Operation                             | Name                    | Rows  | Bytes | Cost (%CPU)| Time     |
-----------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                      |                         |     1 |    25 | 14179   (1)| 00:00:01 |
|   1 |  SORT AGGREGATE                       |                         |     1 |    25 |            |          |
|*  2 |   FILTER                              |                         |       |       |            |          |
|   3 |    MERGE JOIN CARTESIAN               |                         |    10M|   238M| 14177   (1)| 00:00:01 |
|   4 |     TABLE ACCESS FULL                 | TEST_KEYS               |   100 |   300 |     3   (0)| 00:00:01 |
|   5 |     BUFFER SORT                       |                         |   100K|  2148K| 14174   (1)| 00:00:01 |
|   6 |      TABLE ACCESS FULL                | TEST_TABLE              |   100K|  2148K|   142   (0)| 00:00:01 |
|   7 |    TABLE ACCESS BY INDEX ROWID BATCHED| TEST_TABLE_NT           |     2 |    44 |     2   (0)| 00:00:01 |
|*  8 |     INDEX RANGE SCAN                  | SYS_FK0005992996N00002$ |     2 |       |     2   (0)| 00:00:01 |
-----------------------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
   2 - filter("L"."K"MEMBER OFCAST("T"."NT" AS "T_NUM_TAB") )
   8 - access("NESTED_TABLE_ID"=:B1)

--======================= No Index IN Comparison =======================--
select /*+ Test_TAB_IN_LOOKUP_Multi_NO_INDX */ count(t.id) 
from test_table t, table(t.nt) c, test_keys l where c.column_value = l.k;

Plan hash value: 768666713
-----------------------------------------------------------------------------------------------
| Id  | Operation                     | Name          | Rows  | Bytes | Cost (%CPU)| Time     |
-----------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT              |               |     1 |    47 |   304   (0)| 00:00:01 |
|   1 |  SORT AGGREGATE               |               |     1 |    47 |            |          |
|   2 |   NESTED LOOPS                |               |   100 |  4700 |   304   (0)| 00:00:01 |
|   3 |    NESTED LOOPS               |               |   100 |  4700 |   304   (0)| 00:00:01 |
|*  4 |     HASH JOIN                 |               |   100 |  2500 |   254   (0)| 00:00:01 |
|   5 |      TABLE ACCESS FULL        | TEST_KEYS     |   100 |   300 |     3   (0)| 00:00:01 |
|   6 |      TABLE ACCESS FULL        | TEST_TABLE_NT |   200K|  4296K|   251   (0)| 00:00:01 |
|*  7 |     INDEX UNIQUE SCAN         | SYS_C00144532 |     1 |       |     1   (0)| 00:00:01 |
|   8 |    TABLE ACCESS BY INDEX ROWID| TEST_TABLE    |     1 |    22 |     1   (0)| 00:00:01 |
-----------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
   4 - access("C"."COLUMN_VALUE"="L"."K")
   7 - access("C"."NESTED_TABLE_ID"="T"."SYS_NC0000200003$")
   
--======================= Index on (column_value), MEMBER Comparison =======================--
drop index test_table_nt_indx;
create index test_table_nt_indx on test_table_nt(column_value);

begin
   dbms_stats.gather_table_stats(user, 'TEST_TABLE',    cascade=>true);
   dbms_stats.gather_table_stats(user, 'TEST_TABLE_NT', cascade=>true);
end;
/

select /*+ Test_TAB_MEMBER_LOOKUP_Multi_INDX_Col_1 */ count(t.id) 
from test_table t, test_keys l where l.k member of cast(t.nt as t_num_tab);

Plan hash value: 2034372327
-----------------------------------------------------------------------------------------------------------------
| Id  | Operation                             | Name                    | Rows  | Bytes | Cost (%CPU)| Time     |
-----------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                      |                         |     1 |    25 | 14179   (1)| 00:00:01 |
|   1 |  SORT AGGREGATE                       |                         |     1 |    25 |            |          |
|*  2 |   FILTER                              |                         |       |       |            |          |
|   3 |    MERGE JOIN CARTESIAN               |                         |    10M|   238M| 14177   (1)| 00:00:01 |
|   4 |     TABLE ACCESS FULL                 | TEST_KEYS               |   100 |   300 |     3   (0)| 00:00:01 |
|   5 |     BUFFER SORT                       |                         |   100K|  2148K| 14174   (1)| 00:00:01 |
|   6 |      TABLE ACCESS FULL                | TEST_TABLE              |   100K|  2148K|   142   (0)| 00:00:01 |
|   7 |    TABLE ACCESS BY INDEX ROWID BATCHED| TEST_TABLE_NT           |     2 |    44 |     2   (0)| 00:00:01 |
|*  8 |     INDEX RANGE SCAN                  | SYS_FK0005992996N00002$ |     2 |       |     2   (0)| 00:00:01 |
-----------------------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
   2 - filter("L"."K"MEMBER OFCAST("T"."NT" AS "T_NUM_TAB") )
   8 - access("NESTED_TABLE_ID"=:B1)

--======================= Index on (column_value), IN Comparison =======================--
select /*+ Test_TAB_IN_LOOKUP_Multi_INDX_Col_1 */ count(t.id) 
from test_table t, table(t.nt) c, test_keys l where c.column_value = l.k;

Plan hash value: 3838096326
--------------------------------------------------------------------------------------------------------------
| Id  | Operation                               | Name               | Rows  | Bytes | Cost (%CPU)| Time     |
--------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                        |                    |     1 |    47 |   103   (0)| 00:00:01 |
|   1 |  SORT AGGREGATE                         |                    |     1 |    47 |            |          |
|   2 |   NESTED LOOPS                          |                    |   100 |  4700 |   103   (0)| 00:00:01 |
|   3 |    NESTED LOOPS                         |                    |   100 |  4700 |   103   (0)| 00:00:01 |
|   4 |     NESTED LOOPS                        |                    |   100 |  2500 |    53   (0)| 00:00:01 |
|   5 |      TABLE ACCESS FULL                  | TEST_KEYS          |   100 |   300 |     3   (0)| 00:00:01 |
|   6 |      TABLE ACCESS BY INDEX ROWID BATCHED| TEST_TABLE_NT      |     1 |    22 |     1   (0)| 00:00:01 |
|*  7 |       INDEX RANGE SCAN                  | TEST_TABLE_NT_INDX |     1 |       |     1   (0)| 00:00:01 |
|*  8 |     INDEX UNIQUE SCAN                   | SYS_C00144532      |     1 |       |     1   (0)| 00:00:01 |
|   9 |    TABLE ACCESS BY INDEX ROWID          | TEST_TABLE         |     1 |    22 |     1   (0)| 00:00:01 |
--------------------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
   7 - access("C"."COLUMN_VALUE"="L"."K")
   8 - access("C"."NESTED_TABLE_ID"="T"."SYS_NC0000200003$")

--======================= Index on (column_value, nested_table_id), IN Comparison =======================--
drop index test_table_nt_indx;
create index test_table_nt_indx on test_table_nt(column_value, nested_table_id);

begin
   dbms_stats.gather_table_stats(user, 'TEST_TABLE',    cascade=>true);
   dbms_stats.gather_table_stats(user, 'TEST_TABLE_NT', cascade=>true);
end;
/

select /*+ Test_TAB_IN_LOOKUP_Multi_INDX_Col_2 */ count(t.id) 
from test_table t, table(t.nt) c, test_keys l where c.column_value = l.k;

Plan hash value: 2752258125
----------------------------------------------------------------------------------------------------
| Id  | Operation                     | Name               | Rows  | Bytes | Cost (%CPU)| Time     |
----------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT              |                    |     1 |    47 |   103   (0)| 00:00:01 |
|   1 |  SORT AGGREGATE               |                    |     1 |    47 |            |          |
|   2 |   NESTED LOOPS                |                    |   100 |  4700 |   103   (0)| 00:00:01 |
|   3 |    NESTED LOOPS               |                    |   100 |  4700 |   103   (0)| 00:00:01 |
|   4 |     NESTED LOOPS              |                    |   100 |  2500 |    53   (0)| 00:00:01 |
|   5 |      TABLE ACCESS FULL        | TEST_KEYS          |   100 |   300 |     3   (0)| 00:00:01 |
|*  6 |      INDEX RANGE SCAN         | TEST_TABLE_NT_INDX |     1 |    22 |     1   (0)| 00:00:01 |
|*  7 |     INDEX UNIQUE SCAN         | SYS_C00144532      |     1 |       |     1   (0)| 00:00:01 |
|   8 |    TABLE ACCESS BY INDEX ROWID| TEST_TABLE         |     1 |    22 |     1   (0)| 00:00:01 |
----------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
   6 - access("C"."COLUMN_VALUE"="L"."K")
   7 - access("C"."NESTED_TABLE_ID"="T"."SYS_NC0000200003$")
   


3. Nested Table Internals


Referring to early Blog: Foreign Key on Nested Table, we can cross check with Oracle internal table and index DDL:

select o.object_name table_name, c.name column_name, c.type# column_type
  from   dba_objects o, sys.col$ c
 where c.obj# = o.object_id
   and o.object_name in ('TEST_TABLE', 'TEST_TABLE_NT')
 order by o.object_name, c.name;
 
   TABLE_NAME      COLUMN_NAME          COLUMN_TYPE
  --------------- -------------------- -----------
  TEST_TABLE      ID                             2
  TEST_TABLE      NT                           122
  TEST_TABLE      SYS_NC0000200003$             23
  TEST_TABLE_NT   COLUMN_VALUE                   2
  TEST_TABLE_NT   NESTED_TABLE_ID               23

select index_name, table_name, column_name 
  from  dba_ind_columns 
 where table_name in ('TEST_TABLE', 'TEST_TABLE_NT');

  INDEX_NAME                TABLE_NAME      COLUMN_NAME
  ------------------------- --------------- --------------------
  SYS_C00144532             TEST_TABLE      NT
  TEST_TABLE_NT_INDX        TEST_TABLE_NT   COLUMN_VALUE
  TEST_TABLE_NT_INDX        TEST_TABLE_NT   NESTED_TABLE_ID
  SYS_FK0005992996N00002$   TEST_TABLE_NT   NESTED_TABLE_ID

Above xplan shows that for MEMBER Comparison, foreign key index SYS_FK0005992996N00002$ on TEST_TABLE_NT is used, and index on nested table column is not used.

For IN Comparison, when no index on nested table column, TABLE ACCESS FULL on TEST_TABLE_NT is used with table index SYS_C00144532.

When there is Index on (column_value), index TEST_TABLE_NT_INDX and SYS_C00144532 are used, but still need TABLE ACCESS BY INDEX ROWID BATCHED on TEST_TABLE_NT.

When there is Index on (column_value, nested_table_id), index TEST_TABLE_NT_INDX and SYS_C00144532 are used, no more TEST_TABLE_NT access.