Thank you for taking the time to review this question and provide your insight and technique using the rowid.
The three plans in question are as follows.
i) Nested Loops Join & Nested Loops Table Access
-----------------------------------------
|Id | Operation |Name|
-----------------------------------------
| 0| SELECT STATEMENT | |
| 1| NESTED LOOPS | |
| 2| NESTED LOOPS | |
| 3| SORT UNIQUE | |
| 4| TABLE ACCESS FULL |F |
|* 5| INDEX RANGE SCAN |T_IX|
| 6| TABLE ACCESS BY INDEX ROWID|T |
-----------------------------------------
ii) Hash Join Right Semi
-----------------------------------------------
|Id|Operation |Name|
-----------------------------------------------
| 0|SELECT STATEMENT | |
|*1| HASH JOIN RIGHT SEMI | |
| 2| TABLE ACCESS FULL |F |
| 3| TABLE ACCESS BY INDEX ROWID BATCHED|T |
|*4| INDEX FULL SCAN |T_IX|
-----------------------------------------------
iii) Hash "Index" Join & Nested Loops Table Access
------------------------------------------------
| Id | Operation | Name |
------------------------------------------------
| 0 | SELECT STATEMENT | |
| 1 | NESTED LOOPS | |
| 2 | VIEW | VW_NSO_1 |
| 3 | HASH UNIQUE | |
|* 4 | HASH JOIN | |
| 5 | TABLE ACCESS FULL | F |
|* 6 | INDEX FAST FULL SCAN | T_IX |
| 7 | TABLE ACCESS BY USER ROWID| T |
------------------------------------------------
To spare us from working with a 1B row example table, I was trying to scale the problem down while keep the relative proportion.
The original example provided is a 1/1000th scale, so the cardinality is actually
T ~ 1B
T_IX ~ 10M
F ~ 10M
In so doing, I ended up inadvertently making T_IX and F, small enough that the optimizer prefers i) (Nested Loops) over ii) (Hash). In the non-scaled down version, the optimizer prefers ii) (Hash) over i) (Nested Loops).
I am trying to highlight and avoid the 99% inefficiency of plan ii) as the the problem with the current plan - performing a TABLE ACCESS BY INDEX ROWID BATCHED for 10,000 rows to only keep 100. If we increase the cardinality of T_IX and F by a factor of 10 to 100K (instead of 10K) and, to keep T at 1M instead of 1B, we change the proportion to 10% instead of 1%, then we see that the optimizer chooses ii) over i), and iii) has the fewest buffers of them all. Though i) is a close second, and the total memory used of iii) is more than i).
Buffer Summary per Plan with Modified Cardinality and Proportion:
i) 10,798 buffers
ii) 100,000 buffers
iii) 10,420 buffers
If all of the blocks are in memory (no physical reads), then the timing of all plans is less than a second. However, that's not an assumption I tend to make, and my inclination is to assume (the worst case) that all of the blocks are
not in memory.
To avoid the excess block gets on T of plan ii), it seems I either ought to use your rowid technique or use an NL hint. I generally try to avoid using hints (using an NL hint on a billion row table does seem a bit odd), so your rowid technique generating plan iii) seems an attractive solution.
I appreciate and welcome any thoughts or critique you may have to this line of thinking.
Modified setup:
create table T ( id number
, num number
, pad char(2e3) default 'x'
)
/
insert into T ( id, num )
with "D" as
( select level id
from dual connect by level <= 1e3
)
select rownum
, decode( mod( rownum, 1e1 ), 0, rownum )
from "D", "D"
/
create index T_IX on T ( num )
/
create table F ( num number
)
/
insert into F ( num )
with "D" as
( select level id
from dual connect by level <= 1e2
)
select rownum
from "D", "D"
/
insert into F ( num )
with "D" as
( select level id
from dual connect by level <= 3e2
)
select rownum + 1e4
from "D", "D"
/
exec dbms_stats.gather_table_stats( OwnName => user, TabName => 'T' )
exec dbms_stats.gather_table_stats( OwnName => user, TabName => 'F' )
column name format A10
select 'table' type, table_name name, num_rows, sample_size, blocks from user_tables where table_name in( 'T', 'F' )
union all select 'index' type, index_name name, num_rows, sample_size, leaf_blocks from user_indexes where table_name in( 'T' )
order by 1, 2
/
Results for each of the three plans:
Plan i) Nested Loops Join & Nested Loops Table Access - 10,798 buffers.
select /*+ gather_plan_statistics use_nl_with_index ( T T_IX ) leading ( F ) */
id
from "T"
where exists ( select /*+ */
null
from "F"
where T.num = F.num
)
/
----------------------------------------------------------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows |E-Bytes| Cost (%CPU)| E-Time | A-Rows | A-Time | Buffers | OMem | 1Mem | Used-Mem |
----------------------------------------------------------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | | | 100K(100)| | 10000 |00:00:00.12 | 10798 | | | |
| 1 | NESTED LOOPS | | 1 | 100K| 1171K| 100K (1)| 00:00:04 | 10000 |00:00:00.12 | 10798 | | | |
| 2 | NESTED LOOPS | | 1 | 100K| 1171K| 100K (1)| 00:00:04 | 10000 |00:00:00.09 | 798 | | | |
| 3 | SORT UNIQUE | | 1 | 100K| 488K| 178 (2)| 00:00:01 | 100K|00:00:00.04 | 190 | 5439K| 956K| 4834K (0)|
| 4 | TABLE ACCESS FULL | F | 1 | 100K| 488K| 178 (2)| 00:00:01 | 100K|00:00:00.01 | 190 | | | |
|* 5 | INDEX RANGE SCAN | T_IX | 100K| 1 | | 1 (0)| 00:00:01 | 10000 |00:00:00.04 | 608 | | | |
| 6 | TABLE ACCESS BY INDEX ROWID| T | 10000 | 1 | 7 | 2 (0)| 00:00:01 | 10000 |00:00:00.02 | 10000 | | | |
----------------------------------------------------------------------------------------------------------------------------------------------------------
Plan ii) Hash Join Right Semi - 100K buffers.
select /*+ gather_plan_statistics */
id
from "T"
where exists ( select /*+ */
null
from "F"
where T.num = F.num
)
/
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows |E-Bytes| Cost (%CPU)| E-Time | A-Rows | A-Time | Buffers | Reads | OMem | 1Mem | Used-Mem |
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | | | 100K(100)| | 10000 |00:00:00.70 | 100K| 76520 | | | |
|* 1 | HASH JOIN RIGHT SEMI | | 1 | 100K| 1171K| 100K (1)| 00:00:04 | 10000 |00:00:00.70 | 100K| 76520 | 6597K| 3201K| 6933K (0)|
| 2 | TABLE ACCESS FULL | F | 1 | 100K| 488K| 178 (2)| 00:00:01 | 100K|00:00:00.01 | 190 | 0 | | | |
| 3 | TABLE ACCESS BY INDEX ROWID BATCHED| T | 1 | 100K| 683K| 100K (1)| 00:00:04 | 100K|00:00:16.00 | 100K| 76520 | | | |
|* 4 | INDEX FULL SCAN | T_IX | 1 | 100K| | 227 (2)| 00:00:01 | 100K|00:00:00.05 | 225 | 0 | | | |
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------
iii) Hash "Index" Join & Nested Loops Table Access - 10,420 buffers
select /*+ gather_plan_statistics */
id
from "T"
where rowid in ( select T2.rowid
from "T" "T2"
join "F"
on T2.num = F.num
)
/
-------------------------------------------------------------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows |E-Bytes| Cost (%CPU)| E-Time | A-Rows | A-Time | Buffers | OMem | 1Mem | Used-Mem |
-------------------------------------------------------------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | | | 1074 (100)| | 10000 |00:00:00.06 | 10420 | | | |
| 1 | NESTED LOOPS | | 1 | 1 | 29 | 1074 (3)| 00:00:01 | 10000 |00:00:00.06 | 10420 | | | |
| 2 | VIEW | VW_NSO_1 | 1 | 100K| 1171K| 345 (4)| 00:00:01 | 10000 |00:00:00.04 | 420 | | | |
| 3 | HASH UNIQUE | | 1 | 1 | 1855K| | | 10000 |00:00:00.04 | 420 | 1783K| 1783K| 3064K (0)|
|* 4 | HASH JOIN RIGHT SEMI | | 1 | 100K| 1855K| 345 (4)| 00:00:01 | 10000 |00:00:00.02 | 420 | 6597K| 3201K| 6877K (0)|
| 5 | TABLE ACCESS FULL | F | 1 | 100K| 488K| 178 (2)| 00:00:01 | 100K|00:00:00.01 | 190 | | | |
|* 6 | INDEX FAST FULL SCAN | T_IX | 1 | 100K| 1367K| 162 (2)| 00:00:01 | 100K|00:00:00.01 | 230 | | | |
| 7 | TABLE ACCESS BY USER ROWID| T | 10000 | 1 | 17 | 1 (0)| 00:00:01 | 10000 |00:00:00.02 | 10000 | | | |
-------------------------------------------------------------------------------------------------------------------------------------------------------------