从1000s到10s,OceanBase 标量子查询的SQL优化实践

本文作者:胡呈清,爱可生 DBA 团队成员,擅长故障分析、性能优化。

本文借助一个案例,探讨标量子查询执行慢的原因分析过程,以及相应的优化方案。

问题描述

  • 本案例的数据库版本为:OceanBase 3.2.3.3

这个案例里的SQL,执行时间已经超过1000秒。

select
  rq.processinstid processinstid,
  rq.question_id questionId,
  rq.question_no questionNo,
  to_char(rq.rev_start_date, 'yyyy-MM-dd') revStartDate,
  (
    select
      e.name
    from
      e
    where
      e.category_code = 'REV_SOURCE'
      and e.code = rq.rev_source
  ) revSource,
  (
    select
      e.name
    from
      e
    where
      e.category_code = 'QUESTION_TYPE'
      and e.code = rq.question_type
  ) questionType,
  rq.question_summary questionSummary,
  rq.question_desc questionDesc,
  to_char(rq.question_discover_date, 'yyyy-MM-dd') questionDiscoverDate,
  rq.aud_project_type audProjectType,
  (
    select
      d.dept_name
    from
      d
    where
      d.dept_id = rq.check_dept
  ) checkDept,
  (
    select
      to_char(wm_concat(distinct(k.org_name)))
    from
      o,
      k
    where
      o.question_id = rq.question_id
      and o.ASC_ORG = k.org_id
      and o.REFORM_TYPE = '0'
  ) ascOrg,
  (
    select
      to_char(wm_concat(distinct(k.dept_name)))
    from
      o,
      fnd_dept_t k
    where
      o.question_id = rq.question_id
      and o.MAIN_REV_DEPT = k.dept_id
      and o.REFORM_TYPE = '0'
  ) mainRevDept,
  (
    select
      e.name
    from
      e
    where
      e.category_code = 'REV_FINISH_STATE'
      and e.code = rq.rev_finish_state
  ) revFinishState,
  to_char(rq.compliance_date, 'yyyy-MM-dd') complianceDATE
from
  rq
  left join REM_QUESTION_PLAN_T t on rq.question_id = t.question_id
  left join fnd_org_t org on t.ASC_ORG = org.org_id
where
  1 = 1
  and rq.asc_org is null
  and (
    t.asc_org in (
      select
        f.org_id
      from
        f
      where
        f.org_type = 'G'
    )
    or rq.created_by_org in (
      select
        f.org_id
      from
        f
      where
        f.org_type = 'G'
    )
  )
  and rq.company_type = 'G';

分析过程

执行计划如下:

===========================================================
|ID|OPERATOR          |NAME           |EST. ROWS|COST     |
-----------------------------------------------------------
|0 |SUBPLAN FILTER    |               |6283     |788388847|
|1 | SUBPLAN FILTER   |               |6283     |1325483  |
|2 |  HASH OUTER JOIN |               |8377     |210530   |
|3 |   TABLE SCAN     |RQ             |7966     |77932    |
|4 |   TABLE SCAN     |T              |152919   |59150    |
|5 |  TABLE SCAN      |F              |440      |2763     |
|6 |  TABLE SCAN      |F              |440      |2763     |
|7 | TABLE SCAN       |E(SYS_C0011218)|1        |92       |
|8 | TABLE SCAN       |E(SYS_C0011218)|1        |92       |
|9 | TABLE GET        |D              |1        |46       |
|10| SCALAR GROUP BY  |               |1        |62483    |
|11|  NESTED-LOOP JOIN|               |1        |62483    |
|12|   TABLE SCAN     |O              |1        |62468    |
|13|   TABLE GET      |K              |1        |28       |
|14| SCALAR GROUP BY  |               |1        |62483    |
|15|  NESTED-LOOP JOIN|               |1        |62483    |
|16|   TABLE SCAN     |O              |1        |62468    |
|17|   TABLE GET      |K              |1        |27       |
|18| TABLE SCAN       |E(SYS_C0011218)|1        |92       |
===========================================================

每个子算子的成本都不高,但总成本很高!

下面,我们将根据SQL的语法和语义进行解析。

首先,该SQL语句从语法上可以划分为两部分:

  1. 标量子查询,即投影部分的子查询。
  2. 外部查询 ,即 FROM 子句的关联查询和子查询。

因此,这个 SQL 的执行逻辑是(也就是执行计划里的 0 号 SUBPLAN FILTER 算子):

  1. 先执行外部查询 ,得到 结果集 r(执行计划中的 1-6 号算子)。
  2. 再执行标量子查询 ,从 结果集 r 中取一行数据,带入到标量子查询中执行(执行计划中的 7-18 号算子)。
  3. 重复上一步,直到循环取完最后一行数据。

为了定位 SQL 到底慢在哪一步?让我们继续拆解。

  • 先拆出外部查询(即对应的 1-6 号算子部分),单独执行很快得到结果 13 万行,也就意味着所有标量子查询都需要执行 13 万次。
  • 从执行计划来看,7、8、9、18 号算子对应的 4 个标量子查询都可以走索引,效率较高。只保留外部查询和这 4 个标量子查询,执行耗时很短。
  • 重点是 10、14 两个算子,对应的 2 个标量子查询除了和外表关联外,本身内部还有 o、k 这 2 张表关联,这两张表要做多少次关联?13万次! 很明显这里效率会很低。

SQL 中 10、14 两个算子对应的标量子查询如下,还可以再拆解 SQL,单独只做一次 、k 表的关联查询(如下标黄部分)要 200 毫秒:

select
xxx,
(
    select
      to_char(wm_concat(distinct(k.org_name)))
    from
      REM_QUESTION_PLAN_T o,
      fnd_org_t k
    where
      o.question_id = rq.question_id
      and o.ASC_ORG = k.org_id
      and o.REFORM_TYPE = '0'
  ) ascOrg,
  (
    select
      to_char(wm_concat(distinct(k.dept_name)))
    from
      REM_QUESTION_PLAN_T o,
      fnd_dept_t k
    where
      o.question_id = rq.question_id
      and o.MAIN_REV_DEPT = k.dept_id
      and o.REFORM_TYPE = '0'
  ) mainRevDept,
  xxx
  from t(外部查询,结果有 13 万行);

结论

标量子查询的执行计划只能是循环嵌套连接,也就是 SUBPLAN FILTER 算子(等同于 NESTED-LOOP JOIN 执行逻辑),它的执行效率取决于两个因素:

  • 外部查询的结果集大小
  • 子查询的效率

因此,只有当外部查询的结果集规模较小,并且子查询所关联的字段上配置了高效的索引时,整个查询的执行效率才会较高。反之,如果关联字段未建立索引,优化器便无法像处理JOIN 语句那样,有效地利用HASH JOIN等高效算子,从而导致查询的执行效率显著降低。

在上面这个慢 SQL 中,有两个标量子查询不只和外表关联,它内部还有关联查询,所以即使关联字段有索引,子查询单次执行的效率也受限,再加上要执行 13 万次,这个耗时就长了。所以这个 SQL 只能改写成 LEFT JOIN 来优化,这也是标量子查询的标准优化方法。

优化方案

这个 SQL 的标量子查询中有聚合函数,应该先 GROUP BY 聚合后再和外表关联,SQL(局部)改写如下:

with t1 as (
  select
    o.question_id,
    to_char(wm_concat(distinct(k.org_name))) as org_name
  from
    REM_QUESTION_PLAN_T o,
    fnd_org_t k
  where
    o.ASC_ORG = k.org_id
    and o.REFORM_TYPE = '0'
  group by
    o.question_id
),
t2 as (
  select
    o.question_id,
    to_char(wm_concat(distinct(k.dept_name))) as dept_name
  from
    REM_QUESTION_PLAN_T o,
    fnd_dept_t k
  where
    o.MAIN_REV_DEPT = k.dept_id
    and o.REFORM_TYPE = '0'
  group by
    o.question_id
)
select
xxx,
t1.org_name as ascOrg,
t2.dept_name as mainRevDept,
xxx
  from t(外部查询,结果有 13 万行)
  left join t1 on t.question_id=t1.question_id
  left join t2 on t.question_id=t2.question_id;

改写后的执行计划如下(变成了使用 HASH OUTER JOIN 算法),可以看到成本 7.88 亿降到了 365 万,执行耗时降到 10 秒!

=============================================================
|ID|OPERATOR              |NAME           |EST. ROWS|COST   |
-------------------------------------------------------------
|0 |SUBPLAN FILTER        |               |6318     |3653489|
|1 | MERGE GROUP BY       |               |6318     |1636701|
|2 |  SORT                |               |6318     |1632074|
|3 |   SUBPLAN FILTER     |               |6318     |1613799|
|4 |    HASH OUTER JOIN   |               |8424     |492531 |
|5 |     HASH OUTER JOIN  |               |8377     |331672 |
|6 |      MERGE OUTER JOIN|               |7966     |198317 |
|7 |       TABLE SCAN     |RQ             |7966     |77932  |
|8 |       SUBPLAN SCAN   |T2             |2351     |119098 |
|9 |        MERGE GROUP BY|               |2351     |119062 |
|10|         SORT         |               |2352     |118658 |
|11|          HASH JOIN   |               |2352     |113818 |
|12|           TABLE SCAN |K              |22268    |8614   |
|13|           TABLE SCAN |O              |76460    |60075  |
|14|      TABLE SCAN      |T              |152919   |59150  |
|15|     SUBPLAN SCAN     |T1             |76415    |118014 |
|16|      HASH JOIN       |               |76415    |116865 |
|17|       TABLE SCAN     |K              |7033     |2721   |
|18|       TABLE SCAN     |O              |76460    |60075  |
|19|    TABLE SCAN        |F              |440      |2763   |
|20|    TABLE SCAN        |F              |440      |2763   |
|21| TABLE SCAN           |E(SYS_C0011218)|1        |92     |
|22| TABLE SCAN           |E(SYS_C0011218)|1        |92     |
|23| TABLE GET            |D              |1        |46     |
|24| TABLE SCAN           |E(SYS_C0011218)|1        |92     |
=============================================================
相关推荐
OceanBase数据库官方博客2 天前
半连接转内连接 | OceanBase SQL 查询改写
sql·oceanbase·分布式数据库
OceanBase数据库官方博客2 天前
解析在OceanBase创建分区的常见问题|OceanBase 用户问题精粹
oceanbase·分布式数据库·分区
OceanBase数据库官方博客2 天前
半连接转内连接规则的原理与代码解析 |OceanBase查询优化
sql·oceanbase·分布式数据库
IT培训中心-竺老师5 天前
OceanBase 数据库分布式与集中式 能力
数据库·分布式·oceanbase
靖顺5 天前
【OceanBase 诊断调优】—— OceanBase 数据库网络速率配置方案
网络·数据库·oceanbase
Amd7949 天前
索引与性能优化
性能优化·数据库管理·sql优化·数据库索引·数据访问·索引类型·查询效率
尚雷558013 天前
OceanBase 社区版 4.0 离线方式升级bp1至bp2 指南(含避坑总结)
oceanbase
五月高高13 天前
Linux部署oceanbase
linux·oceanbase
靖顺16 天前
【OceanBase 诊断调优】—— 统计信息自动收集超时导致的估行不准 SQL 选择错索引
数据库·sql·oceanbase