OceanBase的SQL 优化实践: NOT IN 子查询

作者:胡呈清,爱可生 DBA 团队成员,擅于故障分析、性能优化。
数据库版本:OceanBase V3.2.3

本文借助一个案例,来了解 not in 对 NULL 值敏感的处理逻辑,并探讨相应的优化方法。

问题概要

前不久,我遇到了一个慢SQL的case,在这段SQL中,原本的NOT IN子查询被优化器改写成了NESTED-LOOP ANTI JOIN,然而,由于被驱动表的全表扫描无法用索引,导致执行耗时达16秒。具体的SQL语句如下:

SELECT AGENT_ID, MAX(REL_AGENT_ID)
            FROM T_LDIM_AGENT_UPREL
           WHERE AGENT_ID NOT IN (select AGENT_ID
                                    from T_LDIM_AGENT_UPREL
                                   where valid_flg = '1')
           group by AGENT_ID;

简略执行计划如下:

==============================================================================================
|ID|OPERATOR              |NAME                                           |EST. ROWS|COST    |
----------------------------------------------------------------------------------------------
|0 |MERGE GROUP BY        |                                               |146      |62970523|
|1 | NESTED-LOOP ANTI JOIN|                                               |149      |62970511|
|2 |  TABLE SCAN          |T_LDIM_AGENT_UPREL(I_LDIM_AGENT_UPREL_AGENT_ID)|27760    |10738   |
|3 |  MATERIAL            |                                               |13880    |11313   |
|4 |   SUBPLAN SCAN       |VIEW1                                          |13880    |11115   |
|5 |    TABLE SCAN        |T_LDIM_AGENT_UPREL                             |13880    |10906   |
==============================================================================================

问题分析

1. 分析表结构、数据量

表结构如下,关联字段 AGENT_ID 是有索引的:

CREATE TABLE "T_LDIM_AGENT_UPREL" (
  "REL_AGENT_ID" NUMBER(22) CONSTRAINT "T_LDIM_AGENT_UPREL_OBNOTNULL_1679987669730612" NOT NULL ENABLE,
  "AGENT_ID" NUMBER(22),
  "EMPLOYEE_ID" NUMBER(22),
  "EMP_PARTY_FULLNAME" VARCHAR2(60),
  "GRP_ID" NUMBER(22),
  "GRP_PARTY_FULLNAME" VARCHAR2(255),
  "CS_ID" NUMBER(22),
  "CS_ORGAN_NAME" VARCHAR2(255),
  "CRT_DTTM" DATE,
  "LASTUPT_DTTM" DATE,
  "VALID_FLG" VARCHAR2(1),
  "VALID_DTTM" DATE,
  "INVALID_DTTM" DATE,
  CONSTRAINT "PK_T_LDIM_AGENT_UPREL" PRIMARY KEY ("REL_AGENT_ID")
);
CREATE INDEX "IDX_T_LDIM_AGENT_UPREL_CT" on "T_LDIM_AGENT_UPREL" ("CRT_DTTM") GLOBAL ;
CREATE INDEX "IDX_T_LDIM_AGENT_UPREL_LT" on "T_LDIM_AGENT_UPREL" ("LASTUPT_DTTM") GLOBAL ;
CREATE INDEX "I_LDIM_AGENT_UPREL_AGENT_ID" on "T_LDIM_AGENT_UPREL" ("AGENT_ID") GLOBAL ;

数据量:T_LDIM_AGENT_UPREL 表一共 2.7 万行,子查询结果 3900 行。

2. 判断直接原因

从执行计划、表结构和数据量来看,这个 SQL 效率低有两个原因:

  1. 关联字段 AGENT_ID 有索引,但对被驱动表做查询时却使用全表扫描,效率必定低。为什么不走索引?
  2. 既然被驱动表不走索引,基于代价的比较,优化器为什么没有选择更高效的 HASH ANTI JOIN?

问题得一个一个看,先分析第二个问题。

3. 使用 HINT 干预 JOIN 算法

使用如下 HINT 都不生效(并且尝试了 Outline Data 中的写法):

/*+ use_hash(A B)*/ 
/*+ USE_HASH(@"SEL$1" ("VIEW1"@"SEL$1" )) */
/*+ NO_USE_NL_AGGREGATION */

执行计划显示 Used Hint 部分都为空,说明 HINT 无法生效,原因未知:

Used Hint:
-------------------------------------
  /*+
  */

Outline Data:
-------------------------------------
  /*+
      BEGIN_OUTLINE_DATA
      NO_USE_HASH_AGGREGATION(@"SEL$1")
      LEADING(@"SEL$1" ("REPORT.A"@"SEL$1" "VIEW1"@"SEL$1" ))
      USE_NL(@"SEL$1" ("VIEW1"@"SEL$1" ))
      PQ_DISTRIBUTE(@"SEL$1" ("VIEW1"@"SEL$1" ) LOCAL LOCAL)
      USE_NL_MATERIALIZATION(@"SEL$1" ("VIEW1"@"SEL$1" ))
      INDEX(@"SEL$1" "REPORT.A"@"SEL$1" "I_LDIM_AGENT_UPREL_AGENT_ID")
      FULL(@"SEL$2" "REPORT.B"@"SEL$2")
      END_OUTLINE_DATA
  */

4. 对比 Oracle 执行计划

Tips:当 OB 上看到的执行计划不符合预期,但又找不到原因时,可以对比 Oracle 的执行计划。

Oracle 上执行计划如下(这里得用 set autotrace on 的方式查看真实执行计划):

  • 可以使用 HASH ANTI JOIN,并且有个重要信息 HASH JOIN RIGHT ANTI NA (EXPLAIN 是看不到 NA 的),

直接搜索就可以得到大概的解释 NA 即 Null-Aware Anti Join,这种反连接能够处理 NULL 值。啥意思?下面展开讲讲。

SQL> set autotrace on
SQL> SELECT AGENT_ID, MAX(REL_AGENT_ID)
            FROM T_LDIM_AGENT_UPREL
           WHERE AGENT_ID NOT IN (select AGENT_ID
                                    from T_LDIM_AGENT_UPREL
                                   where valid_flg = '1')
           group by AGENT_ID;  2    3    4    5    6  

no rows selected

Execution Plan
----------------------------------------------------------
Plan hash value: 1033962367
-----------------------------------------------------------------------------------------------
| Id  | Operation                | Name               | Rows  | Bytes | Cost (%CPU)| Time     |
-----------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT         |                    |     9 |   171 |   276   (2)| 00:00:04 |
|   1 |  HASH GROUP BY           |                    |     9 |   171 |   276   (2)| 00:00:04 |
|*  2 |   HASH JOIN RIGHT ANTI NA|                    |  9672 |   179K|   275   (2)| 00:00:04 |
|*  3 |    TABLE ACCESS FULL     | T_LDIM_AGENT_UPREL |  3886 | 31088 |   137   (1)| 00:00:02 |
|   4 |    TABLE ACCESS FULL     | T_LDIM_AGENT_UPREL | 28098 |   301K|   137   (1)| 00:00:02 |
-----------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------
   2 - access("AGENT_ID"="AGENT_ID")
   3 - filter("VALID_FLG"='1')

5. NULL 值与 NOT IN

为了更好的说明 NULL 值对 NOT IN 的影响,下面举个简单的例子:

create table t1(a number,b varchar2(50),c varchar2(50) not null);
insert into t1 values(1,'aaa','aaa'),(2,'bbb','bbb'),(3,'ccc','ccc'),(4,NULL,'ddd');
commit;

只要 NOT IN 后面的子查询或者常量集合一旦有 NULL 值出现,则整个 SQL 的执行结果就会为 NULL:

obclient [TESTUSER]> select * from t1 where b not in('aaa',NULL);
Empty set (0.004 sec)

obclient [TESTUSER]> select tt.b from t1 tt where tt.a=4;
+------+
| B    |
+------+
| NULL |
+------+
1 row in set (0.007 sec)

obclient [TESTUSER]> select t.* from t1 t where b not in(select tt.b from t1 tt where tt.a=4);
Empty set (0.005 sec)

NOT EXISTS 对 NULL 值不敏感,这意味着 NULL 值对 NOT EXISTS 的执行结果不会有什么影响:

obclient [TESTUSER]> select t.* from t1 t where not EXISTS (select tt.b from t1 tt where t.b=tt.b and tt.a=4);
+------+------+-----+
| A    | B    | C   |
+------+------+-----+
|    1 | aaa  | aaa |
|    2 | bbb  | bbb |
|    3 | ccc  | ccc |
|    4 | NULL | ddd |
+------+------+-----+
4 rows in set (0.005 sec)

IN 对 NULL 值也不敏感:

obclient [TESTUSER]> select * from t1 where b in('aaa',NULL);
+------+------+-----+
| A    | B    | C   |
+------+------+-----+
|    1 | aaa  | aaa |
+------+------+-----+
1 row in set (0.004 sec)

obclient [TESTUSER]> select t.* from t1 t where b in(select tt.b from t1 tt where tt.a<5);
+------+------+-----+
| A    | B    | C   |
+------+------+-----+
|    1 | aaa  | aaa |
|    2 | bbb  | bbb |
|    3 | ccc  | ccc |
+------+------+-----+
3 rows in set (0.002 sec)

结合 Null-Aware Anti Join,我们可以得到如下结论:

NOT IN 和 <>ALL 对 NULL 值敏感,这意味着 NOT IN 后面的子查询或者常量集合一旦有 NULL 值出现,则整个 SQL 的执行结果就会为 NULL。

所以一旦相关的连接列上出现了 NULL 值(实际只会判断字段是否有 NOT NULL 约束),此时 Oracle 如果还按照通常的 ANTI JOIN 的处理逻辑来处理(实际和 INNER JOIN 的处理逻辑一致,差别在于只取不满足关联条件的结果,而 INNER JOIN 对 NULL 值是不敏感的),得到的结果就不对了。

为了解决 NOT IN 和 <>ALL 对 NULL 值敏感的问题,Oracle 推出了改良的 ANTI JOIN(11g 新增了参数 _OPTIMIZER_NULL_AWARE_ANTIJOIN,默认为 true),这种反连接能够处理 NULL 值,Oracle 称其为 Null-Aware Anti Join(在真实的执行计划中显示为 XX ANTI NA)。

6. 小结

到这里我们能解释一个问题:为什么 OB 不能使用 HASH ANTI JOIN ?

原因是关联字段 AGENT_ID 没有 NOT NULL 约束,由于 NOT IN 对 NULL 敏感,不能使用普通的 ANTI JOIN,否则遇到 NULL 结果将不正确。Oracle 11g 推出的 Null-Aware ANTI JOIN 可以处理 NULL 敏感的场景,但是 OB 3.x 还没有这个功能,因此不能使用 HASH ANTI JOIN ,4.x 版本将推出 _OPTIMIZER_NULL_AWARE_ANTIJOIN 参数,和 Oracle 保持一致。

优化建议

既然 NOT IN 对 NULL 敏感,有两个优化方向,先和业务确认 NOT IN 子查询结果集有没有可能出现 NULL,如果不会进一步确认关联字段 AGENT_ID 是否会有 NULL 值,如果不会则下面三种方式任选其一,最佳选择是方法 1,最符合开发规范:

  1. AGENT_ID 字段加上 NOT NULL 约束,这样优化器就可以使用 HASH ANTI JOIN 了;

  2. NOT EXISTS 对 NULL 值不敏感,因此可以将 NOT IN 改写(或者也可以改写成 LEFT JOIN WHERE xx IS NULL 这种 ANTI JOIN 语法):

    SELECT AGENT_ID, MAX(REL_AGENT_ID)
    FROM T_LDIM_AGENT_UPREL t1
    WHERE NOT EXISTS (select AGENT_ID
    from T_LDIM_AGENT_UPREL t2
    where t1.agent_id=t2.agent_id and valid_flg = '1')
    group by AGENT_ID;

改写后的执行计划走了 HASH RIGHT ANTI JOIN,执行耗时只要 50ms:

==========================================================================
|ID|OPERATOR             |NAME                           |EST. ROWS|COST |
--------------------------------------------------------------------------
|0 |HASH GROUP BY        |                               |146      |46828|
|1 | HASH RIGHT ANTI JOIN|                               |149      |46697|
|2 |  SUBPLAN SCAN       |VIEW1                          |13880    |11115|
|3 |   TABLE SCAN        |T2                             |13880    |10906|
|4 |  TABLE SCAN         |T1(I_LDIM_AGENT_UPREL_AGENT_ID)|27760    |10738|
==========================================================================
  1. 给父查询、子查询都加上 AND AGENT_ID is NOT NULL 条件,也可以让优化器走 HASH ANTI JOIN:

    SELECT AGENT_ID, MAX(REL_AGENT_ID)
    FROM T_LDIM_AGENT_UPREL
    WHERE AGENT_ID NOT IN (select AGENT_ID
    from T_LDIM_AGENT_UPREL
    where valid_flg = '1' and AGENT_ID is not null )
    and AGENT_ID is not null
    group by AGENT_ID;

执行计划:

==========================================================================================
|ID|OPERATOR             |NAME                                           |EST. ROWS|COST |
------------------------------------------------------------------------------------------
|0 |HASH GROUP BY        |                                               |146      |47472|
|1 | HASH RIGHT ANTI JOIN|                                               |149      |47341|
|2 |  SUBPLAN SCAN       |VIEW1                                          |13880    |11173|
|3 |   TABLE SCAN        |T_LDIM_AGENT_UPREL                             |13880    |10965|
|4 |  TABLE SCAN         |T_LDIM_AGENT_UPREL(I_LDIM_AGENT_UPREL_AGENT_ID)|27760    |11324|
==========================================================================================

答疑

问题 1. HASH JOIN 只能用于关联条件的等值查询,不支持连接条件是大于、小于、不等于和 LIKE 的场景。为什么 NOT IN、NOT EXISTS 可以使用 HASH ANTI JOIN?

NOT IN、NOT EXISTS 子查询和 WHERE t1.a!=t2.a 看起来相似,但其实语义是不一样的,下面例子可以说明。NOT IN 的语义其实是说如果有相等的值,则外表结果丢弃,因此本质上 NOT IN 的实现方式还是做等值查找,所以 HASH ANTI JOIN 的实现本质和 HASH JOIN 一样,只是在返回结果时做了相反的判断。

obclient [TESTUSER]> select * from t1 t join t1 tt on t.a!=tt.a;
+------+------+-----+------+------+-----+
| A    | B    | C   | A    | B    | C   |
+------+------+-----+------+------+-----+
|    1 | aaa  | aaa |    2 | bbb  | bbb |
|    1 | aaa  | aaa |    3 | ccc  | ccc |
|    1 | aaa  | aaa |    4 | NULL | ddd |
|    2 | bbb  | bbb |    1 | aaa  | aaa |
|    2 | bbb  | bbb |    3 | ccc  | ccc |
|    2 | bbb  | bbb |    4 | NULL | ddd |
|    3 | ccc  | ccc |    1 | aaa  | aaa |
|    3 | ccc  | ccc |    2 | bbb  | bbb |
|    3 | ccc  | ccc |    4 | NULL | ddd |
|    4 | NULL | ddd |    1 | aaa  | aaa |
|    4 | NULL | ddd |    2 | bbb  | bbb |
|    4 | NULL | ddd |    3 | ccc  | ccc |
+------+------+-----+------+------+-----+
12 rows in set (0.005 sec)

obclient [TESTUSER]> select t.* from t1 t where a not in(select tt.a from t1 tt);
Empty set (0.005 sec)

这个还可以用 Oracle 的执行计划和优化报告来验证:

##执行计划的2号算子 HASH JOIN RIGHT ANTI NA 有如下条件,这里能说明是做的等值查找
2 - access("AGENT_ID"="AGENT_ID")

##另外可以通过下面方法查看优化器改写后的SQL:
alter session set tracefile_identifier='10053c';
alter session set events '10053 trace name context forever,level 1';
执行 SQL;                             
alter session set events '10053 trace name context off';
cd /u01/oracle/diag/rdbms/repo/repo/trace
cat repo_ora_6702_10053c.trc 在 "Final query after transformations" 部分即为优化器改写后的SQL,关联条件也是等值查询:
Final query after transformations:******* UNPARSED QUERY IS *******
SELECT "T_LDIM_AGENT_UPREL"."AGENT_ID" "AGENT_ID",MAX("T_LDIM_AGENT_UPREL"."REL_AGENT_ID") "MAX(REL_AGENT_ID)" FROM "REPORT"."T_LDIM_AGENT_UPREL" "T_LDIM_AGENT_UPREL","REPORT"."T_LDIM_AGENT_UPREL" "T_LDIM_AGENT_UPREL" WHERE "T_LDIM_AGENT_UPREL"."AGENT_ID"="T_LDIM_AGENT_UPREL"."AGENT_ID" AND "T_LDIM_AGENT_UPREL"."VALID_FLG"='1' GROUP BY "T_LDIM_AGENT_UPREL"."AGENT_ID"
kkoqbc: optimizing query block SEL$5DA710D3 (#1)

问题 2. 为什么 OB 可以使用 NESTED-LOOP ANTI JOIN?它能处理 NULL 敏感?怎么实现的?因为它的实现方式导致了对被驱动表只能全表扫描不能走索引?

从结果来看,OB 的 NESTED-LOOP ANTI JOIN 查询结果正确,能处理 NULL 敏感。

实现方式可以从执行计划看出一些端倪:

==============================================================================================
|ID|OPERATOR              |NAME                                           |EST. ROWS|COST    |
----------------------------------------------------------------------------------------------
|0 |MERGE GROUP BY        |                                               |146      |62970523|
|1 | NESTED-LOOP ANTI JOIN|                                               |149      |62970511|
|2 |  TABLE SCAN          |T_LDIM_AGENT_UPREL(I_LDIM_AGENT_UPREL_AGENT_ID)|27760    |10738   |
|3 |  MATERIAL            |                                               |13880    |11313   |
|4 |   SUBPLAN SCAN       |VIEW1                                          |13880    |11115   |
|5 |    TABLE SCAN        |T_LDIM_AGENT_UPREL                             |13880    |10906   |
==============================================================================================

Outputs & filters: 
-------------------------------------
  0 - output([T_LDIM_AGENT_UPREL.AGENT_ID(0x7eeef19c3fe0)], [T_FUN_MAX(T_LDIM_AGENT_UPREL.REL_AGENT_ID(0x7eeef19c50f0))(0x7eeef19c49e0)]), filter(nil), 
      group([T_LDIM_AGENT_UPREL.AGENT_ID(0x7eeef19c3fe0)]), agg_func([T_FUN_MAX(T_LDIM_AGENT_UPREL.REL_AGENT_ID(0x7eeef19c50f0))(0x7eeef19c49e0)])
  1 - output([T_LDIM_AGENT_UPREL.AGENT_ID(0x7eeef19c3fe0)], [T_LDIM_AGENT_UPREL.REL_AGENT_ID(0x7eeef19c50f0)]), filter(nil), 
      conds([(T_OP_OR, T_LDIM_AGENT_UPREL.AGENT_ID(0x7eeef19c3fe0) = VIEW1.AGENT_ID(0x7eeef19ce070)(0x7eeef19ce360), (T_OP_IS, T_LDIM_AGENT_UPREL.AGENT_ID(0x7eeef19c3fe0), NULL, 0)(0x7eeef19cf2e0), (T_OP_IS, VIEW1.AGENT_ID(0x7eeef19ce070), NULL, 0)(0x7eeef19cfee0))(0x7eeef19cec00)]), nl_params_(nil), batch_join=false
  2 - output([T_LDIM_AGENT_UPREL.AGENT_ID(0x7eeef19c3fe0)], [T_LDIM_AGENT_UPREL.REL_AGENT_ID(0x7eeef19c50f0)]), filter(nil), 
      access([T_LDIM_AGENT_UPREL.AGENT_ID(0x7eeef19c3fe0)], [T_LDIM_AGENT_UPREL.REL_AGENT_ID(0x7eeef19c50f0)]), partitions(p0), 
      is_index_back=false, 
      range_key([T_LDIM_AGENT_UPREL.AGENT_ID(0x7eeef19c3fe0)], [T_LDIM_AGENT_UPREL.REL_AGENT_ID(0x7eeef19c50f0)]), range(MIN,MIN ; MAX,MAX)always true
  3 - output([VIEW1.AGENT_ID(0x7eeef19ce070)]), filter(nil)
  4 - output([VIEW1.AGENT_ID(0x7eeef19ce070)]), filter(nil), 
      access([VIEW1.AGENT_ID(0x7eeef19ce070)])
  5 - output([T_LDIM_AGENT_UPREL.AGENT_ID(0x7eeef1a609a0)]), filter([T_LDIM_AGENT_UPREL.VALID_FLG(0x7eeef1a606b0) = ?(0x7eeef1a60c90)]), 
      access([T_LDIM_AGENT_UPREL.VALID_FLG(0x7eeef1a606b0)], [T_LDIM_AGENT_UPREL.AGENT_ID(0x7eeef1a609a0)]), partitions(p0), 
      is_index_back=false, filter_before_indexback[false], 
      range_key([T_LDIM_AGENT_UPREL.REL_AGENT_ID(0x7eeef1a821a0)]), range(MIN ; MAX)always true

把 1 号 NESTED-LOOP ANTI JOIN 算子的 Outputs & filters 单独拿出来看:

 1 - output([T_LDIM_AGENT_UPREL.AGENT_ID(0x7eeef19c3fe0)], [T_LDIM_AGENT_UPREL.REL_AGENT_ID(0x7eeef19c50f0)]), filter(nil), 
      conds([(T_OP_OR, T_LDIM_AGENT_UPREL.AGENT_ID(0x7eeef19c3fe0) = VIEW1.AGENT_ID(0x7eeef19ce070)(0x7eeef19ce360), (T_OP_IS, T_LDIM_AGENT_UPREL.AGENT_ID(0x7eeef19c3fe0), NULL, 0)(0x7eeef19cf2e0), (T_OP_IS, VIEW1.AGENT_ID(0x7eeef19ce070), NULL, 0)(0x7eeef19cfee0))(0x7eeef19cec00)]), nl_params_(nil), batch_join=false

匹配条件是:

where T_LDIM_AGENT_UPREL.AGENT_ID=VIEW1.AGENT_ID 
Or T_LDIM_AGENT_UPREL.AGENT_ID is NULL -- 判断父查询AGENT_ID是否为空,如果遇到 NULL值,则剔除这行结果
Or VIEW1.AGENT_ID is NULL -- 判断子查询结果集 AGENT_ID是否为 NULL,如果遇到NULL值,直接进入JOIN_END阶段,不返回任何数据

以上逻辑是可以实现 NULL 值敏感的。

按照这个逻辑,即使加上 Or VIEW1.AGENT_ID IS NULL 条件,被驱动表依然是可以使用索引的,只有 IS NOT NULL 无法使用索引:

##SQL
select AGENT_ID from T_LDIM_AGENT_UPREL 
where AGENT_ID='124253' or AGENT_ID is null;

##执行计划
==============================================================================
|ID|OPERATOR  |NAME                                           |EST. ROWS|COST|
------------------------------------------------------------------------------
|0 |TABLE SCAN|T_LDIM_AGENT_UPREL(I_LDIM_AGENT_UPREL_AGENT_ID)|1        |46  |
==============================================================================

Outputs & filters: 
-------------------------------------
  0 - output([T_LDIM_AGENT_UPREL.AGENT_ID(0x7eef739f9120)]), filter(nil), 
      access([T_LDIM_AGENT_UPREL.AGENT_ID(0x7eef739f9120)]), partitions(p0), 
      is_index_back=false, 
      range_key([T_LDIM_AGENT_UPREL.AGENT_ID(0x7eef739f9120)], [T_LDIM_AGENT_UPREL.REL_AGENT_ID(0x7eef73a40830)]), range(124253,MIN ; 124253,MAX), (NULL,MIN ; NULL,MAX), 
      range_cond([T_LDIM_AGENT_UPREL.AGENT_ID(0x7eef739f9120) = ?(0x7eef739f7a50) OR (T_OP_IS, T_LDIM_AGENT_UPREL.AGENT_ID(0x7eef739f9120), NULL, 0)(0x7eef739f86d0)(0x7eef739f6dd0)])

按照经验,此时我们应该到 Oracle 上看看 NESTED-LOOP ANTI JOIN NA 的处理逻辑,不过在 Oracle 上调不出这个执行计划,因此线索中断。

推断:目前 3.x 版本没有实现真正意义上的 NESTED-LOOP ANTI JOIN NA,但是 NESTED-LOOP ANTI JOIN 可以正确处理 NULL 敏感。4.x 会实现 NESTED-LOOP ANTI JOIN NA,实现方式就是我们前面推理出的逻辑,也就是说 3.x 用的不是这一套逻辑,执行计划虽然这么显示,但实际不一样,对被驱动表匹配查询时就是要遍历全表,不能直接走索引匹配。

问题 3. 加 /*+ no_rewrite */后,走 SUBPLAN FILTER 算子,父查询显示可以走索引,为什么执行效率还是慢?

/*+ no_rewrite */ 的执行计划,执行耗时 7 秒,比原始 SQL 耗时 16 秒快,从执行逻辑来看:

  1. 这里是非相关子查询,每次重复执行的结果都是一样的,所以执行一次后保存在参数集合中(init_plan_idxs_([1]) 表示子查询只需要执行一次)。

  2. 从参数中拿到右边非相关子查询的结果,下推 FILTER 到左边计划,执行父查询,注意看条件是 A.AGENT_ID!= ALL(subquery(1)),这里是 !=,因此无法使用索引快速过滤数据,需要扫描整个索引,所以执行效率并不高。如果这里不是 NOT IN 而是 IN,则可以走索引快速查找。

    ======================================================================
    |ID|OPERATOR |NAME |EST. ROWS|COST |

    |0 |MERGE GROUP BY | |3659 |58062035|
    |1 | SUBPLAN FILTER| |13880 |58061224|
    |2 | TABLE SCAN |A(I_LDIM_AGENT_UPREL_AGENT_ID)|27760 |10738 |
    |3 | TABLE SCAN |B |13880 |10906 |

    Outputs & filters:

    0 - output([A.AGENT_ID(0x7ee843c44330)], [T_FUN_MAX(A.REL_AGENT_ID(0x7ee843c45440))(0x7ee843c44d30)]), filter(nil),
    group([A.AGENT_ID(0x7ee843c44330)]), agg_func([T_FUN_MAX(A.REL_AGENT_ID(0x7ee843c45440))(0x7ee843c44d30)])
    1 - output([A.AGENT_ID(0x7ee843c44330)], [A.REL_AGENT_ID(0x7ee843c45440)]), filter([A.AGENT_ID(0x7ee843c44330) != ALL(subquery(1)(0x7ee843bf8e60))(0x7ee843bf8470)]),
    exec_params_(nil), onetime_exprs_(nil), init_plan_idxs_([1])
    2 - output([A.AGENT_ID(0x7ee843c44330)], [A.REL_AGENT_ID(0x7ee843c45440)]), filter(nil),
    access([A.AGENT_ID(0x7ee843c44330)], [A.REL_AGENT_ID(0x7ee843c45440)]), partitions(p0),
    is_index_back=false,
    range_key([A.AGENT_ID(0x7ee843c44330)], [A.REL_AGENT_ID(0x7ee843c45440)]), range(MIN,MIN ; MAX,MAX)always true
    3 - output([B.AGENT_ID(0x7ee843c41350)]), filter([B.VALID_FLG(0x7ee843c40c40) = ?(0x7ee843c40520)]),
    access([B.VALID_FLG(0x7ee843c40c40)], [B.AGENT_ID(0x7ee843c41350)]), partitions(p0),
    is_index_back=false, filter_before_indexback[false],
    range_key([B.REL_AGENT_ID(0x7ee843cb5bb0)]), range(MIN ; MAX)always true

相关推荐
OceanBase数据库官方博客1 天前
半连接转内连接 | OceanBase SQL 查询改写
sql·oceanbase·分布式数据库
OceanBase数据库官方博客2 天前
解析在OceanBase创建分区的常见问题|OceanBase 用户问题精粹
oceanbase·分布式数据库·分区
OceanBase数据库官方博客2 天前
半连接转内连接规则的原理与代码解析 |OceanBase查询优化
sql·oceanbase·分布式数据库
IT培训中心-竺老师5 天前
OceanBase 数据库分布式与集中式 能力
数据库·分布式·oceanbase
靖顺5 天前
【OceanBase 诊断调优】—— OceanBase 数据库网络速率配置方案
网络·数据库·oceanbase
Amd7948 天前
索引与性能优化
性能优化·数据库管理·sql优化·数据库索引·数据访问·索引类型·查询效率
尚雷558012 天前
OceanBase 社区版 4.0 离线方式升级bp1至bp2 指南(含避坑总结)
oceanbase
五月高高12 天前
Linux部署oceanbase
linux·oceanbase
靖顺16 天前
【OceanBase 诊断调优】—— 统计信息自动收集超时导致的估行不准 SQL 选择错索引
数据库·sql·oceanbase