半连接、反连接的优化案例

先来理解几个概念:

半连接:两表关联,只返回匹配上的数据并且只会返回一张的表的数据,半连接一般就是指的在子查询中出现 IN 和 EXISTS

反连接:两表关联,只返回主表的数据,并且只返回主表与子表没关联上的数据,这种连接就叫反连接。反连接一般就是指的 NOT IN 和 NOT EXISTS

子查询展开:优化器将嵌套的子查询展开成一个等价的JOIN,然后去优化这个JOIN。如果不展开的情形是,从主表中获取的每1条数据,都要代入子查询进行匹配。一般情况下效率都是比较低的,这时候执行计划中会出现关键字FILTER。当子查询不展开,SQL无法从连接方式、连接顺序 等方面优化整个查询。因为这些都是固定的了

排序合并连接:排序合并连接原理是先对两个表/行源根据JOIN列进行排序(当然了排序的时候要踢出不符合where条件的列),然后再进行连接。排序合并连接只适用于非等值JOIN。。根据排序合并的原理,我们知道排序合并连接其实很耗费资源,因为要对2个表/结果集进行排序,2个表都需要放入PGA,所以一般情况下,CBO是不会选择走SORT MERGE JOIN

案例背景:某系统新上上了一条SQL语句,结果跑不出来.......

INSERT INTO AAA_AAAA.AAA_MID_FFF
  SELECT S.AS_OF_DATE,
         S.CUST_ACCT_NO,
         S.GL_ACCT_ID,
         S.ORGIN_DT,
         S.DUE_DT,
         S.TRAN_METHOD,
         S.INTEREST_RATE,
         S.TRAN_END_RATE,
         S.INTEREST_RATE - S.TRAN_END_RATE MARGIN_RATE
    FROM AAA_AAAA.AAA_MID_INSTAAAAAA S
   WHERE S.CUST_ACCT_NO NOT IN
         (SELECT DISTINCT T.CUST_ACCT_NO FROM AAA_MID_FFF T)
     AND S.MA_ACCT_NO IN
         (SELECT
           MA_ACCT_NO
            FROM (SELECT R.MA_ACCT_NO,
                         ROW_NUMBER() OVER(PARTITION BY R.CUST_ACCT_NO ORDER BY R.ORGIN_DT) RN
                    FROM AAA_MID_FFF_RESULT R
                   WHERE R.USED_TRAN_METHOD = 'Repricing_Term') M
           WHERE M.RN = 1);
Plan hash value: 466080511
 
---------------------------------------------------------------------------------------------------
| Id  | Operation                    | Name               | Rows  | Bytes | Cost (%CPU)| Time     |
---------------------------------------------------------------------------------------------------
|   0 | INSERT STATEMENT             |                    |     1 |   513 |   416   (1)| 00:00:05 |
|   1 |  LOAD TABLE CONVENTIONAL     | AAA_MID_FFF        |       |       |            |          |
|*  2 |   FILTER                     |                    |       |   513 |   416   (1)| 00:00:05 |
|*  3 |    HASH JOIN ANTI NA         |                    |     1 |   655 |   416   (1)| 00:00:05 |
|   4 |     TABLE ACCESS FULL        | AAA_MID_INSTAAAAAA |     1 |   495 |     2   (0)| 00:00:01 |
|   5 |     TABLE ACCESS FULL        | AAA_MID_FFF        |  107K | 1893K |     2   (0)| 00:00:01 |
|*  6 |    VIEW                      |                    |     1 |   155 |     3  (34)| 00:00:01 |
|*  7 |     WINDOW SORT PUSHED RANK  |                    |     1 |   355 |     3  (34)| 00:00:01 |
|*  8 |      TABLE ACCESS FULL       | AAA_MID_FFF_RESULT |     1 |   355 |     2   (0)| 00:00:01 |
---------------------------------------------------------------------------------------------------
 
Predicate Information (identified by operation id):
---------------------------------------------------
 
   2 - filter(EXISTS (SELECT 0 FROM (SELECT "S"."MA_ACCT_NO" "MA_ACCT_NO",ROW_NUMBER() OVER(
   					PARTITION BY "S"."CUST_ACCT_NO" ORDER BY "S"."ORGIN_DT") "RN" FROM
   					"ANNE_WORK"."AAA_MID_FFF_RESULT" "S" WHERE "S"."USED_TRAN_METHOD"='Repricing_Term') "M" WHERE
   					"MA_ACCT_NO"=:B1 AND "M"."RN"=1))
   3 - access("S"."CUST_ACCT_NO"="S"."CUST_ACCT_NO" AND
   					SUBSTR("CUST_ACCT_NO",1,16)=SUBSTR("S"."CUST_ACCT_NO",1,16))
   6 - filter("MA_ACCT_NO"=:B1 AND "M"."RN"=1)
   7 - filter(ROW_NUMBER() OVER ( PARTITION BY "R"."CUST_ACCT_NO" ORDER BY 
              "R"."ORGIN_DT")<=1)
   8 - filter("R"."USED_TRAN_METHOD"='Repricing_Term')

执行计划 Id =2 FILTER  谓词信息

2 - filter(EXISTS (SELECT 0 FROM (SELECT "S"."MA_ACCT_NO" "MA_ACCT_NO",ROW_NUMBER() OVER(
                       PARTITION BY "S"."CUST_ACCT_NO" ORDER BY "S"."ORGIN_DT") "RN" FROM
                       "ANNE_WORK"."AAA_MID_FFF_RESULT" "S" WHERE "S"."USED_TRAN_METHOD"='Repricing_Term') "M" WHERE "MA_ACCT_NO"=:B1 AND "M"."RN"=1))

子查询作为一个整体,没有展开。FILTER称为改良的嵌套循环,可以把其当成NEST LOOP来优化。首先查看驱动表返回的行数。这个驱动表是AAA_MID_INSTAAAAAA和AAA_MID_FFF作HASH连接的结果集,分别COUNT(*) 两表 结果分别是4kw、100k,而100k的表是用来抵消的表(类似于出问题的流水记录的表),也就是说是为了流水号不重复构造的表。所以对这种表的反连接不会缩小整个结果集的量级。所以HASH连接的结果集接近AAA_MID_INSTAAAAAA的COUNT,4kw行,作为NEST LOOP的驱动表,4kw次循环。前面说了FILTER 无法更改连接方式、连接顺序,常用的的方法就是改写(使用WITH AS 的方式。) 这里面我们使用HINT 让子查询展开

INSERT INTO AAA_AAAA.AAA_MID_FFF
  SELECT S.AS_OF_DATE,
         S.CUST_ACCT_NO,
         S.GL_ACCT_ID,
         S.ORGIN_DT,
         S.DUE_DT,
         S.TRAN_METHOD,
         S.INTEREST_RATE,
         S.TRAN_END_RATE,
         S.INTEREST_RATE - S.TRAN_END_RATE MARGIN_RATE
    FROM AAA_AAAA.AAA_MID_INSTAAAAAA S
   WHERE S.CUST_ACCT_NO NOT IN
         (SELECT DISTINCT T.CUST_ACCT_NO FROM AAA_MID_FFF T)
     AND S.MA_ACCT_NO IN
         (SELECT
          /*+ unnest */
           MA_ACCT_NO
            FROM (SELECT R.MA_ACCT_NO,
                         ROW_NUMBER() OVER(PARTITION BY R.CUST_ACCT_NO ORDER BY R.ORGIN_DT) RN
                    FROM AAA_MID_FFF_RESULT R
                   WHERE R.USED_TRAN_METHOD = 'Repricing_Term') M
           WHERE M.RN = 1);

Plan hash value: 2087877310
 
--------------------------------------------------------------------------------------------------------
| Id  | Operation                      | Name                  | Rows  | Bytes | Cost (%CPU)| Time     |
--------------------------------------------------------------------------------------------------------
|   0 | INSERT STATEMENT               |                       |     1 |   655 |   415   (1)| 00:00:05 |
|   1 |  LOAD TABLE CONVENTIONAL       | AAA_MID_FFF           |       |       |            |          |
|*  2 |   HASH JOIN ANTI NA            |                       |     1 |   655 |   415   (1)| 00:00:05 |
|   3 |    MERGE JOIN SEMI             |                       |     1 |   637 |     4  (50)| 00:00:01 |
|   4 |     TABLE ACCESS BY INDEX ROWID| AAA_MID_INSTAAAAAA    |     1 |   495 |     0   (0)| 00:00:01 |
|   5 |      INDEX FULL SCAN           | PK_AAA_MID_INSTAAAAAA |     1 |       |     0   (0)| 00:00:01 |
|*  6 |     SORT UNIQUE                |                       |     1 |   142 |     4  (50)| 00:00:01 |
|   7 |      VIEW                      | VW_NSO_1              |     1 |   142 |     3  (34)| 00:00:01 |
|*  8 |       VIEW                     |                       |     1 |   155 |     3  (34)| 00:00:01 |
|*  9 |        WINDOW SORT PUSHED RANK |                       |     1 |   355 |     3  (34)| 00:00:01 |
|* 10 |         TABLE ACCESS FULL      | AAA_MID_FFF_RESULT    |     1 |   355 |     2   (0)| 00:00:01 |
|  11 |    TABLE ACCESS FULL           | AAA_MID_FFF           |   107K|  1893K|   410   (1)| 00:00:05 |
--------------------------------------------------------------------------------------------------------
 
Predicate Information (identified by operation id):
---------------------------------------------------
 
   2 - access("S"."CUST_ACCT_NO"="T"."CUST_ACCT_NO" AND 
              SUBSTR("CUST_ACCT_NO",1,16)=SUBSTR("T"."CUST_ACCT_NO",1,16))
   6 - access("S"."MA_ACCT_NO"="MA_ACCT_NO")
       filter("S"."MA_ACCT_NO"="MA_ACCT_NO")
   8 - filter("M"."RN"=1)
   9 - filter(ROW_NUMBER() OVER ( PARTITION BY "R"."CUST_ACCT_NO" ORDER BY "R"."ORGIN_DT")<=1)
  10 - filter("R"."USED_TRAN_METHOD"='Repricing_Term')

注意一下这个HINT的写法和位置(HINT只对当前查询块有效),加完HINT之后的执行计划确实消除了引起性能问题的"FILTER"但是效果并不是太好,仍然需要9000s+才返回结果。根据常识 这个数据量正常应该在10分钟以内响应。

从执行计划中我们可以看出“好久不见”的MERGE JOIN。上面我们说了这个SORT MERGE JOIN的原理,这里我们说一下这个连接方式的适用场景:只适用于非等值连接,这也是他唯一的适用场景。换句话说,如果是等值连接,HASH的效率远高于SMJ,除非连接列都已经排序而且内存足够大。当然这里面的瓶颈是Id =4、5,我们发现4、5前面没有* ,没有过滤,这种情况下对索引INDEX FULL SCAN,然后又全部回表。这两个操作都是单块读。粗略估计一下扫描了2倍原表数据块,而且是单块读。所以耗费的时间相当于扫描一次4000w行的全表的128*2倍。这里面我们只需要FULL(S) 让其全表扫描即可,加上HINT之后的执行计划

INSERT INTO AAA_AAAA.AAA_MID_FFF
  SELECT /*+ FULL(S) */S.AS_OF_DATE,
         S.CUST_ACCT_NO,
         S.GL_ACCT_ID,
         S.ORGIN_DT,
         S.DUE_DT,
         S.TRAN_METHOD,
         S.INTEREST_RATE,
         S.TRAN_END_RATE,
         S.INTEREST_RATE - S.TRAN_END_RATE MARGIN_RATE
    FROM AAA_AAAA.AAA_MID_INSTAAAAAA S
   WHERE S.CUST_ACCT_NO NOT IN
         (SELECT DISTINCT T.CUST_ACCT_NO FROM AAA_MID_FFF T)
     AND S.MA_ACCT_NO IN
         (SELECT
          /*+ unnest */
           MA_ACCT_NO
            FROM (SELECT R.MA_ACCT_NO,
                         ROW_NUMBER() OVER(PARTITION BY R.CUST_ACCT_NO ORDER BY R.ORGIN_DT) RN
                    FROM AAA_MID_FFF_RESULT R
                   WHERE R.USED_TRAN_METHOD = 'Repricing_Term') M
           WHERE M.RN = 1);
           
Plan hash value: 258356950
 
---------------------------------------------------------------------------------------------------
| Id  | Operation                    | Name               | Rows  | Bytes | Cost (%CPU)| Time     |
---------------------------------------------------------------------------------------------------
|   0 | INSERT STATEMENT             |                    |     1 |   655 |   416   (1)| 00:00:05 |
|   1 |  LOAD TABLE CONVENTIONAL     | AAA_MID_FFF        |       |       |            |          |
|*  2 |   HASH JOIN ANTI NA          |                    |     1 |   655 |   416   (1)| 00:00:05 |
|*  3 |    HASH JOIN SEMI            |                    |     1 |   637 |     5  (20)| 00:00:01 |
|   4 |     TABLE ACCESS FULL        | AAA_MID_INSTAAAAAA |     1 |   495 |     2   (0)| 00:00:01 |
|   5 |     VIEW                     | VW_NSO_1           |     1 |   142 |     3  (34)| 00:00:01 |
|*  6 |      VIEW                    |                    |     1 |   155 |     3  (34)| 00:00:01 |
|*  7 |       WINDOW SORT PUSHED RANK|                    |     1 |   355 |     3  (34)| 00:00:01 |
|*  8 |        TABLE ACCESS FULL     | AAA_MID_FFF_RESULT |     1 |   355 |     2   (0)| 00:00:01 |
|   9 |    TABLE ACCESS FULL         | AAA_MID_FFF        |   107K|  1893K|   410   (1)| 00:00:05 |
---------------------------------------------------------------------------------------------------
 
Predicate Information (identified by operation id):
---------------------------------------------------
 
   2 - access("S"."CUST_ACCT_NO"="T"."CUST_ACCT_NO" AND 
              SUBSTR("CUST_ACCT_NO",1,16)=SUBSTR("T"."CUST_ACCT_NO",1,16))
   3 - access("S"."MA_ACCT_NO"="MA_ACCT_NO")
   6 - filter("M"."RN"=1)
   7 - filter(ROW_NUMBER() OVER ( PARTITION BY "R"."CUST_ACCT_NO" ORDER BY 
              "R"."ORGIN_DT")<=1)
   8 - filter("R"."USED_TRAN_METHOD"='Repricing_Term')

这时候我们发现执行计划居然走回了HASH JOIN这种正常的连接,为什么?

------------------------------------------------------------------------思考一下------------------------------------------------------------------------

因为在等值连接的情况下,如果关联列有序,CBO则认为SMJ的效率可能会高于HASH,案例中,下面分析函数的结果集已经排序,上面的表连接列上面有索引,而索引的存储是有序的 ,所以正好满足了CBO认为的使用SMJ的条件。当我们强制S表全表扫描之后,CBO无法获取有序的数据,所以连接方式换成了HASH JOIN

猜你喜欢

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