非相关查询不应该在 not in 语句中只执行一次吗?

Shouldn't non correlated queries be executed only once in a not in statement?

最近我遇到了一个我无法解释的奇怪行为,需要一些帮助来理解为什么会这样。

想象以下场景:我想检索在用户无权访问的所有房间中发生的所有 classes。为此,我在如下查询中使用 not in

select id 
  from class
 where room_id not in 
     (
         select room_id 
           from user_room
          where user_id = 123
     )

我虽然因为内部查询(在 not in 内)独立于外部查询(非相关查询),内部查询只会执行一次,但实际情况是它正在对 class table 中的每条记录执行一次。这会导致性能下降。

我说它对每个 class 条记录执行一次的原因是因为查询的解释计划如下:

-------------------------------------------------------------------------------------
| Id  | Operation          | Name           | Rows  | Bytes | Cost (%CPU)| Time     |
-------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT   |                |     1 |    59 |   144   (0)| 00:00:01 |
|   1 |  NESTED LOOPS ANTI |                |     1 |    59 |   144   (0)| 00:00:01 |
|   2 |   TABLE ACCESS FULL| CLASS          |   137 |  6302 |   144   (0)| 00:00:01 |
|*  3 |   INDEX UNIQUE SCAN| USER_ROOM_UK   |     4 |    52 |     0   (0)| 00:00:01 |
-------------------------------------------------------------------------------------

我假设 ID 为 1 的步骤是对 table class 的每条记录执行的 not in 查询。我的解释正确吗?

如果我用查询返回的值替换 not in 查询,类似于:

select id 
  from class
 where room_id not in 
     (
         1, 2, 3
     )

解释计划现在显示:

---------------------------------------------------------------------------
| Id  | Operation         | Name  | Rows  | Bytes | Cost (%CPU)| Time     |
---------------------------------------------------------------------------
|   0 | SELECT STATEMENT  |       |   125 |  5750 |   144   (0)| 00:00:01 |
|*  1 |  TABLE ACCESS FULL| CLASS |   125 |  5750 |   144   (0)| 00:00:01 |
---------------------------------------------------------------------------

所以这就是为什么我假设对外部查询的每条记录执行一次内部查询。

我想了解为什么会这样。它不应该只执行一次,因为我正在处理不相关的查询吗?还是我的假设有误?

另外,有没有办法告诉 Oracle Engine 只执行一次内部查询?

欢迎任何反馈! 谢谢!

Is my interpretation correct?

您可以通过使用 gather_plan_statistics 提示执行查询来检查这一点

select /*+ gather_plan_statistics */ /* my_mark01 */id 
  from class c
 where c.room_id not in
 (
     select room_id
       from user_room ur
      where user_id = 123
 );

select t.*
  from v$sql s, table(dbms_xplan.display_cursor(s.sql_id, null, 'allstats last')) t
 where s.sql_text like '%my_mark01%'
   and not s.sql_text like '%v$sql%';

这将向您显示每个计划行的 Starts 指标,以了解实际执行了多少次以及 A-rows指标将显示实际获取的行数。

通常 Oracle 足够聪明,可以避免在这种情况下做额外的工作。例如。从描述中修改查询(使用它是因为问题查询在我的测试环境中给出了完全不同和正确的计划)

create table class (id number(10), room_id number(10), description varchar2(50));
create table user_room (user_id number(10), room_id number(10), description varchar2(50));
create unique index user_room_uk on user_room(user_id, room_id) tablespace drnindexes;

insert into class
select level, trunc((level-1)/100)+1, level||' '||(trunc((level-1)/100)+1) from dual connect by level <= 500;
commit;

insert into user_room
select (case when level <= 3 then 123 else 345 end), level, null from dual connect by level <= 5;
commit;    

select /*+ gather_plan_statistics */ /* my_mark02 */id 
  from class c
 where not exists
 (
     select 1
       from user_room ur
      where ur.user_id = 123
        and ur.room_id = c.room_id
 );
     
 select t.*
 from v$sql s, table(dbms_xplan.display_cursor(s.sql_id, null, 'allstats last')) t
 where s.sql_text like '%my_mark02%'
   and not s.sql_text like '%v$sql%';

显示

SQL_ID  fu0qyzn2anmgm, child number 0
-------------------------------------
select /*+ gather_plan_statistics */ /* my_mark02 */id 
   from class 
c
  where not exists
  (
      select 1
        from user_room ur
      
 where ur.user_id = 123
         and ur.room_id = c.room_id
  )
 
Plan hash value: 300864768
 
---------------------------------------------------------------------------------------------
| Id  | Operation          | Name         | Starts | E-Rows | A-Rows |   A-Time   | Buffers |
---------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT   |              |      1 |        |    200 |00:00:00.01 |      29 |
|   1 |  NESTED LOOPS ANTI |              |      1 |    500 |    200 |00:00:00.01 |      29 |
|   2 |   TABLE ACCESS FULL| CLASS        |      1 |    500 |    500 |00:00:00.01 |      26 |
|*  3 |   INDEX UNIQUE SCAN| USER_ROOM_UK |      5 |      1 |      3 |00:00:00.01 |       3 |
---------------------------------------------------------------------------------------------
 
Predicate Information (identified by operation id):
---------------------------------------------------
 
   3 - access("UR"."USER_ID"=123 AND "UR"."ROOM_ID"="C"."ROOM_ID")
 
Note
-----
   - dynamic statistics used: dynamic sampling (level=2)

尽管从 Class table 中检索到的行数是 500 - user_room table 扫描的开始计数仅为 5 - 不同 room_id class 中的值 table.

why this is happening

通常这种情况的发生是因为 tables/indexes 的统计信息不正确或在特定实例上配置了一些特定的优化器设置参数。 您是否尝试收集描述的 table 的统计数据以实现它?

Also, is there a way to tell Oracle Engine to execute the inner query only once?

如果我理解正确的话——子查询会给你相对较少的行数。在这种情况下,您可以尝试通过使用 hash_aj hint 强制 Oracle 使用散列反连接 喜欢(不幸的是,不能在我的环境中正确测试,因为问题不会重现并且 oracle select hash join anti na 自动)

select  id 
  from class c
 where c.room_id not in
 (
     select /*+ hash_aj swap_join_inputs(ur) */ room_id
       from user_room ur
      where user_id = 123
 );