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

问题描述

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

想象一下以下情况:我想检索用户无法访问的所有房间中发生的所有类。为此,我在查询中使用not in,如下所示:

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

我虽然那是因为内部查询(在not in内部)与外部查询(非相关查询)是独立的,所以内部查询只会执行一次,但是实际上是一次执行了对于类表中的每个记录。这会严重影响性能

我之所以说对每个类记录都执行一次,是因为查询的解释计划如下:

-------------------------------------------------------------------------------------
| 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的步骤是对表类的每个记录执行的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仅执行一次内部查询一次吗?

任何反馈表示赞赏! 谢谢!

解决方法

我的解释正确吗?

您可以通过使用如下所示的 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%';

这将为您显示每个计划行的开始指标,以了解其实际执行了多少次,而 A行指标将显示实际获取的行数。

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

create table class (id number(10),room_id number(10),description varchar2(50));
create table user_room (user_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,'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)

从类表中检索的行数为500-user_room表扫描的开始计数仅为5-类表中不同的room_id值的数量。

为什么会这样

通常是由于对表/索引的统计不正确或在特定实例上配置的某些特定的优化器设置参数而发生这种情况。 您是否尝试收集描述表的统计数据以实现它?

此外,是否有一种方法可以告诉Oracle Engine执行内部查询 只有一次?

如果我正确理解-子查询将为您提供相对少量的行。在这种情况下,您可以尝试通过使用hash_aj hint来强制Oracle使用哈希反连接。 像(不幸的是,由于问题无法重现,并且oracle自动选择了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
 );