【问题标题】:Shouldn't non correlated queries be executed only once in a not in statement?不相关的查询不应该在 not in 语句中只执行一次吗?
【发布时间】:2020-08-12 11:12:03
【问题描述】:

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

想象以下场景:我想检索在用户无权访问的所有房间中发生的所有类。为此,我在如下查询中使用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 引擎只执行一次内部查询?

感谢任何反馈! 谢谢!

【问题讨论】:

  • 您在此处标记了 2 个完全不同的 RDBMS。您实际使用的是哪个?
  • @Larnu 对不起,很糟糕。感谢您的提醒!

标签: query-performance oracle19c


【解决方案1】:

我的解释正确吗?

您可以通过使用 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 表中检索到的行数为 500 - user_room 表扫描的 Starts 计数仅为 5 - class 表中不同 room_id 值的数量。

为什么会这样

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

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

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

【讨论】:

  • 非常感谢您的意见!我阅读了您发送的关于查询取消嵌套的链接,我意识到即使内部查询是不相关的,它也不会被 Oracle 引擎取消嵌套,因为它被用于 NOT IN。只有在 EXISTS 或 IN 中使用时,非相关查询才会被取消嵌套。使用您提供的提示解决了我的问题。我将通过您的回答意识到的另一件事是,这也可以通过 NOT EXISTS 来实现,我将比较两者的性能并使用最快的。非常感谢您的帮助!!!
猜你喜欢
  • 1970-01-01
  • 2021-05-20
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-04-06
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多