一次执行计划突变的故障分析

一,     现象描述

2018年5月23日 20:30分理财系统出现超时现象,原本10-20分钟执行完成的过程持续了很长时间(3小时)。

1)定位超时的会话及语句:

通过自己开发的管控工具-快速问题定位模块,

l定位到当前会话为975,

l语句是一条UPDATE语句:

update t5_cust_vol c
   set c.cust_manager =
       (select case
                 when tl.cust_manager is null then
                  c.cust_manager
                 else
                  tl.cust_manager
               end
          from t5_cust_trans_log tl
         where tl.trans_serno =
               (select max(tt.trans_serno)
                  from t5_cust_trans_log tt
                 where tt.cust_no = c.cust_no
                   and (tt.cust_manager is not null or
                       trim(tt.cust_manager) is not null)
                   and tt.busi_code in ('130', '122')
                   and tt.trans_status in ('0', '3')
                   and tt.capital_status = '3'
                   and tt.ack_date = :sWorkdate
                   and tt.prod_code = c.prod_code))
 where exists (select 1
          from t5_cust_trans_log l
         where l.cust_no = c.cust_no
           and (l.cust_manager is not null or
               trim(l.cust_manager) is not null)
           and l.busi_code in ('130', '122')
           and l.trans_status in ('0', '3')
           and l.capital_status = '3'
           and l.ack_date = :sWorkdate2
           and l.prod_code = c.prod_code)

2)查看当前语句的执行时间:

该语句执行时间是17:47分,已经运行了将近3个小时,采用的执行计划是1056208194

3)查看该语句的历史执行时间:

在节点1   5.21日的执行时间9分钟左右, 执行计划为: 1879483692

在节点2   5.18日的执行时间20分钟左右;执行计划为:1056208194

4)查看该语句的历史执行计划

Plan hash value: 1056208194
------------------------------------------------------------------------------------------------------------
| Id  | Operation                        | Name                    | Rows  | Bytes | Cost (%CPU)| Time     |
------------------------------------------------------------------------------------------------------------
|   0 | UPDATE STATEMENT                 |                         |       |       |  4709 (100)|          |
|   1 |  UPDATE                          | T5_CUST_VOL             |       |       |            |          |
|   2 |   NESTED LOOPS                   |                         |       |       |            |          |
|   3 |    NESTED LOOPS                  |                         |     1 |    72 |  4692   (1)| 00:00:57 |
|   4 |     SORT UNIQUE                  |                         |     1 |    42 |  4049   (1)| 00:00:49 |
|   5 |      INLIST ITERATOR             |                         |       |       |            |          |
|   6 |       TABLE ACCESS BY INDEX ROWID| T5_CUST_TRANS_LOG       |     1 |    42 |  4049   (1)| 00:00:49 |
|   7 |        INDEX RANGE SCAN          | IX_T5_CUST_TRANS_LOG_02 | 30723 |       |   207   (1)| 00:00:03 |
|   8 |     INDEX RANGE SCAN             | PK_T5_CUST_VOL          |   637 |       |     8   (0)| 00:00:01 |
|   9 |    TABLE ACCESS BY INDEX ROWID   | T5_CUST_VOL             |     1 |    30 |   642   (0)| 00:00:08 |
|  10 |   TABLE ACCESS BY INDEX ROWID    | T5_CUST_TRANS_LOG       |     1 |    27 |     3   (0)| 00:00:01 |
|  11 |    INDEX UNIQUE SCAN             | PK_T5_CUST_TRANS_LOG    |     1 |       |     2   (0)| 00:00:01 |
|  12 |     SORT AGGREGATE               |                         |     1 |    67 |            |          |
|  13 |      INLIST ITERATOR             |                         |       |       |            |          |
|  14 |       TABLE ACCESS BY INDEX ROWID| T5_CUST_TRANS_LOG       |     1 |    67 |    14   (0)| 00:00:01 |
|  15 |        INDEX RANGE SCAN          | IX_T5_CUST_TRANS_LOG_02 |    59 |       |     6   (0)| 00:00:01 |
------------------------------------------------------------------------------------------------------------

 
Plan hash value: 1879483692
------------------------------------------------------------------------------------------------------------
| Id  | Operation                        | Name                    | Rows  | Bytes | Cost (%CPU)| Time     |
------------------------------------------------------------------------------------------------------------
|   0 | UPDATE STATEMENT                 |                         |       |       | 22943 (100)|          |
|   1 |  UPDATE                          | T5_CUST_VOL             |       |       |            |          |
|   2 |   HASH JOIN RIGHT SEMI           |                         |   123 |  8610 | 15317   (1)| 00:03:04 |
|   3 |    TABLE ACCESS FULL             | T5_CUST_TRANS_LOG       |   122 |  4880 |  9731   (1)| 00:01:57 |
|   4 |    TABLE ACCESS FULL             | T5_CUST_VOL             |  1166K|    33M|  5578   (1)| 00:01:07 |
|   5 |   TABLE ACCESS BY INDEX ROWID    | T5_CUST_TRANS_LOG       |     1 |    32 |     3   (0)| 00:00:01 |
|   6 |    INDEX UNIQUE SCAN             | PK_T5_CUST_TRANS_LOG    |     1 |       |     2   (0)| 00:00:01 |
|   7 |     SORT AGGREGATE               |                         |     1 |    65 |            |          |
|   8 |      INLIST ITERATOR             |                         |       |       |            |          |
|   9 |       TABLE ACCESS BY INDEX ROWID| T5_CUST_TRANS_LOG       |     1 |    65 |    59   (0)| 00:00:01 |
|  10 |        INDEX RANGE SCAN          | IX_T5_CUST_TRANS_LOG_02 |    76 |       |     6   (0)| 00:00:01 |
------------------------------------------------------------------------------------------------------------

===================================分析原因===================================

一、为什么 1879483692 比 1056208194 的效率高

1.首先查看相关表大小   和  行数

    T5_CUST_VOL                119W 行  176MB

    T5_CUST_TRANS_LOG              7.9W 行  322MB

HASH JOIN RIGHT SEMI所消耗的时间 约等于 全表扫描两个表所消耗的时间(驱动表和被驱动表都只访问一次) 

正常情况下 在普通的机械盘读取1MB的数据 大约用时10ms 所以这种方式 最多消耗时间为5s  

随着数据量的增大对这种连接方式影响不大。除非驱动表T5_CUST_TRANS_LOG 增大到1.5倍PGA大小 才可能会对SQL产生质变影响  

NESTED LOOPS效率取决于 驱动表T5_CUST_TRANS_LOG返回的结果集行数(一般在10000条以后性能就开始急剧下降)。

查询所得T5_CUST_TRANS_LOG返回的结果集为3w+条,超过了10000行多倍,所以执行效率相当低下。

而且 从执行计划ID=7可以看出 对于驱动表的访问使用了索引IX_T5_CUST_TRANS_LOG_02

查询索引信息可得 索引是使用busi_code作为引导列

查看BUSI_CODE列的选择性
select BUSI_CODE,count(*) from T5_CUST_TRANS_LOG group by BUSI_CODE order by 2 desc 
BUSI_CODE    COUNT(*)
130    33565
124    3661
122    3515
127    1640
131    700
152    264
200    122
125    84
126    78
132    7
202    3

可以看出索引列非常不均匀,而且条件and tt.busi_code in ('130', '122') 是从7.9w行数据中 过滤出4w行数据,选择性非常差而且需要回表

这个比全表扫描的效率还低很多。还好表数据不大,没有造成巨大的性能问题。但是如果表数据量增加。性能问题就越来越大

使用实验验证我们的分析结论
===========================================使用nest loop的方式==========================================
SQL> set timing on
SQL> SELECT /*+ USE_NL(TT C) LEADING(TT)*/COUNT(1)
  2     FROM t5_cust_vol c, t5_cust_trans_log tt
  3    WHERE tt.prod_code = c.prod_code
  4      and tt.busi_code in ('130', '122')
  5      and tt.trans_status in ('0', '3')
  6      and tt.capital_status = '3'
  7      and tt.cust_no = c.cust_no ;
  COUNT(1)
----------
      1232
Executed in 606.002 seconds

SELECT * FROM TABLE(DBMS_XPLAN.display);
Plan hash value: 3553801761
---------------------------------------------------------------------------------------------------
| Id  | Operation                     | Name              | Rows  | Bytes | Cost (%CPU)| Time     |
---------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT              |                   |     1 |    54 |    17M  (1)| 58:47:11 |
|   1 |  SORT AGGREGATE               |                   |     1 |    54 |            |          |
|   2 |   NESTED LOOPS                |                   |       |       |            |          |
|   3 |    NESTED LOOPS               |                   | 29709 |  1566K|    17M  (1)| 58:47:11 |
|*  4 |     TABLE ACCESS FULL         | T5_CUST_TRANS_LOG | 29709 |   899K| 11118   (1)| 00:02:14 |
|*  5 |     INDEX RANGE SCAN          | PK_T5_CUST_VOL    |   615 |       |     7   (0)| 00:00:01 |
|*  6 |    TABLE ACCESS BY INDEX ROWID| T5_CUST_VOL       |     1 |    23 |   593   (0)| 00:00:08 |
---------------------------------------------------------------------------------------------------
 
Predicate Information (identified by operation id):
---------------------------------------------------
 
   4 - filter("TT"."CAPITAL_STATUS"='3' AND ("TT"."BUSI_CODE"='122' OR 
              "TT"."BUSI_CODE"='130') AND ("TT"."TRANS_STATUS"='0' OR "TT"."TRANS_STATUS"='3'))
   5 - access("TT"."PROD_CODE"="C"."PROD_CODE")
   6 - filter("TT"."CUST_NO"="C"."CUST_NO")

============================================使用HASH的方式===============================================
SQL> set timing on
SQL> SELECT /*+ USE_HASH(TT C) LEADING(TT)*/COUNT(1)
  2     FROM t5_cust_vol c, t5_cust_trans_log tt
  3    WHERE tt.prod_code = c.prod_code
  4      and tt.busi_code in ('130', '122')
  5      and tt.trans_status in ('0', '3')
  6      and tt.capital_status = '3'
  7      and tt.cust_no = c.cust_no ;
  COUNT(1)
----------
      1232
Executed in 0.515 seconds

Plan hash value: 3548041056
-----------------------------------------------------------------------------------------
| Id  | Operation           | Name              | Rows  | Bytes | Cost (%CPU)| Time     |
-----------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT    |                   |     1 |    54 | 17241   (1)| 00:03:27 |
|   1 |  SORT AGGREGATE     |                   |     1 |    54 |            |          |
|*  2 |   HASH JOIN         |                   | 29709 |  1566K| 17241   (1)| 00:03:27 |
|*  3 |    TABLE ACCESS FULL| T5_CUST_TRANS_LOG | 29709 |   899K| 11118   (1)| 00:02:14 |
|   4 |    TABLE ACCESS FULL| T5_CUST_VOL       |  1192K|    26M|  6115   (1)| 00:01:14 |
-----------------------------------------------------------------------------------------
 
Predicate Information (identified by operation id):
---------------------------------------------------
 
   2 - access("TT"."PROD_CODE"="C"."PROD_CODE" AND "TT"."CUST_NO"="C"."CUST_NO")
   3 - filter("TT"."CAPITAL_STATUS"='3' AND ("TT"."BUSI_CODE"='122' OR 
              "TT"."BUSI_CODE"='130') AND ("TT"."TRANS_STATUS"='0' OR "TT"."TRANS_STATUS"='3'))

二、为何使用NEST LOOP的执行计划1056208194 的执行效率非常不稳定(20min - 3小时)

  NEST LOOP的原理是驱动表返回多少行,被驱动表t5_cust_vol就会被扫描多少次

  当T5_CUST_TRANS_LOG返回结果集10000行时 整个nest loop的性能就会急剧下降。

  (比如 驱动表返回结果集为100行 SQL用时1s  当返回10000行  用时就是1s*100=100s)

  所以nest loop下面的2个点会导致执行效率不稳定

    a).驱动表返回的结果集远大于10000行 而且浮动很大

    b).被驱动表的连接列没有索引(假设驱动表返回10000行来说 被驱动表的连接列有索引就会 使用索引扫描10000次 

如果没有索引就会全表扫描10000次 两者的区别相当于全表扫描和索引扫描的效率区别再放大10000倍。所以这个一旦出问题就会引起巨大的性能问题)

按照上面的分析我们做如下检查

A).查询出问题的当天和正常天 驱动表返回的结果集的有什么不同
SELECT macdate, COUNT(1)
  FROM t5_cust_trans_log TT
WHERE tt.busi_code in ('130', '122')
  and tt.trans_status in ('0', '3')
  and tt.capital_status = '3'
GROUP BY macdate
ORDER BY 1 DESC;
macdate  count(*)
----------------------
20180524    4038
20180523    8198---31893
20180522    6819---23695
20180521    5822
20180520    1644
20180519    2307
20180518    3173---7103
20180517    2928
20180516    1001
20170711    1

上面的查询结果展现的是每日的新增量

从管控工具里面查到20180518 SQL运行了20分钟 此时驱动表的返回结果为 7103行(<10000行)

出问题的当天为20180523 此时驱动表的返回结果为31893行,驱动表返回的行数增加了4.5倍

理论上来说效率的差异应该在4.5倍左右,也就是最多90分钟肯定可以执行完,不至于会拖到3小时

B).查询被驱动表连接列上使用的索引是否合理 
从执行计划ID=8可以看出被驱动表使用了主键索引PK_T5_CUST_VOL并在ID=9进行了回表操作,索引元数据信息如下:
-----------------------------------------------------------------------------------------
alter table T5_CUST_VOL
  add constraint PK_T5_CUST_VOL primary key (PROD_CODE, FNC_TRANS_ACCT_NO, DISTRIBUTOR_CODE, SDCSH_FLG)
  using index tablespace FNC_DATA
-----------------------------------------------------------------------------------------

查询索引引导列PROD_CODE的选择性查看索引是否合理
select prod_code,count(*) from t5_cust_vol group by prod_code
PROD_CODE        COUNT(*)
------------- --------
1510320000102    11132
1610621000101    2576
1811111000529    9805
1810111016413    22213
1810111006836    11
1810411002706    872
1711211004917    27
1710111007827    1
1810714000604    3
1711511001213    420
1810111001535    14
1810111001550    270
1810111003938    77
1810111003826    87
1810111015205    1894
1710313020301    1
1810313007101    1
1810313008501    1
...
...

可以看出引导列分布相当不均匀,执行如下查询查一下不均匀的跨度
select prod_code,count(*) from t5_cust_vol group by prod_code order by 2 desc
PROD_CODE    COUNT(*)
1611420000101    80205
2012127000101    52575
1710111033926    25774
1810111016413    22213
1516220000201    21904
...
1810111015509    17136
1811511000137    16984
1711511003237    16261
1810111016513    15931
1810111014805    14559
1811111000717    12672
...
...
1810313008501    1

引导列的分布不均匀是导致SQL不稳定的主要原因:

表总行数119w行,通过索引返回1行数据、10000行数据、80000行数据 的效率差异非常大

因为回表(table access by index rowid)的数据块会相差很大

1行数据 回表的只需要扫描1个block  也就是一个逻辑读

10000行数据(就算1个block存100行数据)需要扫描100个block 也就是100个逻辑读,同理80000行数据  需要800个逻辑读

因为数据不均匀 SQL的执行效率会差距上百倍,但是这是嵌套循环一次的差异。这个SQL需要嵌套循环31893次。。。。

其实SQL的关联条件有两个 where tt.cust_no = c.cust_no and tt.prod_code = c.prod_code 顺便查一下cust_no的选择性

select cust_no,count(*) from t5_cust_vol group by cust_no order by 2 desc
CUST_NO    COUNT(*)
01489784    42
00045501    37
00307879    33
00087550    33
00323364    31
00016414    30
00387624    30
...
01520411    24
00779185    24
00241608    24
00661209    23
00703982    23
00773559    23
01661044    23
01822613    23
00129400    23
...
...

从上面的查询结果可以看出CUST_NO的选择性非常好,而且数据分布很均匀

如果建立(cust_no,prod_code)组合索引 则根本不可能出现执行效率不稳定的情况

===================================持续优化===================================

因为update机制类似于nest loop,所以SQL的写法决定了SQL仍然存在潜在性能问题

由上面分析可知走HASH的执行计划为效率最高最稳定的执行计划

Plan hash value: 1879483692
------------------------------------------------------------------------------------------------------------
| Id  | Operation                        | Name                    | Rows  | Bytes | Cost (%CPU)| Time     |
------------------------------------------------------------------------------------------------------------
|   0 | UPDATE STATEMENT                 |                         |       |       | 22943 (100)|          |
|   1 |  UPDATE                          | T5_CUST_VOL             |       |       |            |          |
|   2 |   HASH JOIN RIGHT SEMI           |                         |   123 |  8610 | 15317   (1)| 00:03:04 |
|   3 |    TABLE ACCESS FULL             | T5_CUST_TRANS_LOG       |   122 |  4880 |  9731   (1)| 00:01:57 |
|   4 |    TABLE ACCESS FULL             | T5_CUST_VOL             |  1166K|    33M|  5578   (1)| 00:01:07 |
|   5 |   TABLE ACCESS BY INDEX ROWID    | T5_CUST_TRANS_LOG       |     1 |    32 |     3   (0)| 00:00:01 |
|   6 |    INDEX UNIQUE SCAN             | PK_T5_CUST_TRANS_LOG    |     1 |       |     2   (0)| 00:00:01 |
|   7 |     SORT AGGREGATE               |                         |     1 |    65 |            |          |
|   8 |      INLIST ITERATOR             |                         |       |       |            |          |
|   9 |       TABLE ACCESS BY INDEX ROWID| T5_CUST_TRANS_LOG       |     1 |    65 |    59   (0)| 00:00:01 |
|  10 |        INDEX RANGE SCAN          | IX_T5_CUST_TRANS_LOG_02 |    76 |       |     6   (0)| 00:00:01 |
------------------------------------------------------------------------------------------------------------

ID=2的兄弟节点是ID=5 他们共同的父亲节点是ID=1 UPDATE(UPDATE的内部实现机制就是NEST LOOP)

所以ID=2返回的结果集行数大小 直接会影响整个SQL的性能

ID=2 HASH JOIN RIGHT SEMI的结果集实际返回了1200行(<<10000行)

如果某一天随着数据量增大 返回的结果集>10000行 就开始出现性能问题,然后越来越明显

为了以绝后患 可以采用merge等价改写的方式 来彻底消除SQL的潜在风险

merge /*+ USE_HASH(C) */ into t5_cust_vol c
using (select tl.cust_manager, tl.cust_no, tl.prod_code
         from t5_cust_trans_log tl,
              (select max(tt.trans_serno) trans_serno
                 from t5_cust_trans_log tt
                where (tt.cust_manager is not null or
                      trim(tt.cust_manager) is not null)
                  and tt.busi_code in ('130', '122')
                  and tt.trans_status in ('0', '3')
                  and tt.capital_status = '3'
                  and tt.ack_date = :sWorkdate < char(255) >
                group by tt.cust_no, tt.prod_code) ttt
        where ttt.trans_serno = tl.trans_serno) x
on (x.cust_no = c.cust_no and x.prod_code = c.prod_code)
when matched then
  update set c.cust_manager = x.cust_manager

猜你喜欢

转载自blog.csdn.net/Skybig1988/article/details/81239011