特殊分页语句的改写优化

甲方某一个客户反应一个报表页面查询点击之后需要等很久(半分钟)才能显示数据,点击页面生成的SQL如下:

 

SELECT ROWNUM R, EE.*
  FROM (SELECT E.*
          FROM (SELECT C.*, PI.CN_NAME
                  FROM CCCCCC_FFFF_INFO C, DDDDD_INFO PI
                 WHERE PI.PARTY_ID = C.PARTY_ID
                UNION ALL
                SELECT C.*, PI.CN_NAME
                  FROM H_CCCCCC_FFFF_INFO C, DDDDD_INFO PI
                 WHERE PI.PARTY_ID = C.PARTY_ID) E
         ORDER BY E.TRAN_TIME DESC, E.SYS_SEQ_ID DESC) EE
 WHERE ROWNUM <= 20

PLAN如下:

 

Plan hash value: 1127495981
 
------------------------------------------------------------------------------------------------------
| Id  | Operation               | Name               | Rows  | Bytes |TempSpc| Cost (%CPU)| Time     |
------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT        |                    |    20 | 75520 |       |   718K  (1)| 02:23:43 |
|*  1 |  COUNT STOPKEY          |                    |       |       |       |            |          |
|   2 |   VIEW                  |                    |   909K|  3276M|       |   718K  (1)| 02:23:43 |
|*  3 |    SORT ORDER BY STOPKEY|                    |   909K|  3276M|  7107M|   718K  (1)| 02:23:43 |
|   4 |     VIEW                |                    |   909K|  3276M|       |    27   (4)| 00:00:01 |
|   5 |      UNION-ALL          |                    |       |       |       |            |          |
|*  6 |       HASH JOIN         |                    |   909K|   677M|       |    14   (8)| 00:00:01 |
|   7 |        TABLE ACCESS FULL| DDDDD_INFO         |   482 |  9158 |       |    11   (0)| 00:00:01 |
|   8 |        TABLE ACCESS FULL| CCCCCC_FFFF_INFO   |    21 |  8001 |       |     2   (0)| 00:00:01 |
|*  9 |       HASH JOIN         |                    |   117 | 88101 |       |    14   (8)| 00:00:01 |
|  10 |        TABLE ACCESS FULL| DDDDD_INFO         |   482 |  9158 |       |    11   (0)| 00:00:01 |
|  11 |        TABLE ACCESS FULL| H_CCCCCC_FFFF_INFO |   117 | 42939 |       |     2   (0)| 00:00:01 |
------------------------------------------------------------------------------------------------------
 
Predicate Information (identified by operation id):
---------------------------------------------------
 
   1 - filter(ROWNUM<=20)
   3 - filter(ROWNUM<=20)
   6 - access("PI"."PARTY_ID"="C"."PARTY_ID")
   9 - access("PI"."PARTY_ID"="C"."PARTY_ID")

为了后面能够更直观的对比:我这里顺便把SQL真实的PLAN取出来。如下:

 

 

Plan hash value: 1127495981

-----------------------------------------------------------------------------------------------------------------------------------
| Id  | Operation               | Name               | Starts | E-Rows | A-Rows |   A-Time   | Buffers |  OMem |  1Mem | Used-Mem |
-----------------------------------------------------------------------------------------------------------------------------------
|*  1 |  COUNT STOPKEY          |                    |      1 |        |     20 |00:00:01.14 |   48469 |       |       |          |
|   2 |   VIEW                  |                    |      1 |  20679 |     20 |00:00:01.14 |   48469 |       |       |          |
|*  3 |    SORT ORDER BY STOPKEY|                    |      1 |  20679 |     20 |00:00:01.14 |   48469 | 55296 | 55296 |49152  (0)|
|   4 |     VIEW                |                    |      1 |  20679 |   1457 |00:00:01.06 |   48469 |       |       |          |
|   5 |      UNION-ALL          |                    |      1 |        |   1457 |00:00:01.05 |   48469 |       |       |          |
|*  6 |       HASH JOIN         |                    |      1 |  20676 |   1457 |00:00:01.05 |   48407 |  1023K|  1023K| 1120K (0)|
|   7 |        TABLE ACCESS FULL| DDDDD_INFO         |      1 |     11 |     11 |00:00:00.01 |      46 |       |       |          |
|   8 |        TABLE ACCESS FULL| CCCCCC_FFFF_INFO   |      1 |    885 |    918K|00:00:00.92 |   48361 |       |       |          |
|*  9 |       HASH JOIN         |                    |      1 |      3 |      0 |00:00:00.01 |      62 |  1023K|  1023K| 1120K (0)|
|  10 |        TABLE ACCESS FULL| DDDDD_INFO         |      1 |     11 |     11 |00:00:00.01 |      46 |       |       |          |
|  11 |        TABLE ACCESS FULL| H_CCCCCC_FFFF_INFO |      1 |    117 |    117 |00:00:00.01 |      16 |       |       |          |
-----------------------------------------------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   1 - filter(ROWNUM<=20)
   3 - filter(ROWNUM<=20)
   6 - access("PI"."PARTY_ID"="C"."PARTY_ID")
   9 - access("PI"."PARTY_ID"="C"."PARTY_ID")

逻辑读4.8w,响应时间是 3s

 

 

 

先总结一下分页语句的特点及优化技巧:

1.分页语句一般情况下服务于页面请求,对于响应时间要求高,一旦响应时间过长,客户不明所以的可能会反复点击。对数据库造成类似于“阻塞”的等待

2.分页语句里面必须含有order by,如果没有order by可以说分页是没有意义的

3.分页语句的主要消耗在order by,优化分页语句主要目的就是消除排序带来的性能问题

4.因为索引在创建的时候已经排序,所以可以通过在order by的列上面建索引的方式来抵消排序

5.建索引的时候升序或者降序 最好和order by后面的升序或者降序保持一致

6.如果SQL语句还有额外的过滤,建组合索引的时候应该把过滤列放在第一位

综上所述:分页语句优化的精髓就是通过建索引抵消掉排序带来的性能开销

但是!!上述分页SQL是取两个表关联的结果union all,然后去order by这个结果集(所以plan里面占用到很大的Temp)

要消除这个结果集的order by,建索引显然是不行的。

针对这个语句 我提供了三个解决方案:

1、利用数据库对象优化:将这俩表CCCCCC_FFFF_INFO,H_CCCCCC_FFFF_INFO UNION ALL的结果集做成一个物化视图(视图需要维护成本大),在物化视图上面建索引去消除排序。

2、通过业务层处理优化:通过了解业务H_xxx表是历史表,CCCCCC_FFFF_INFO的当天的交易流水表。可以通过将CCCCCC_FFFF_INFO实时或者定时同步到历史表。原SQL中union all当天流水表的部分就可以去掉。这个就变成了我们可优化的分页框架

3、SQL层等价改写:将上述SQL先进行等价改写,改写成常规可优化的分页语句,再通过建索引优化。

 

通过讨论觉得第二种方法最妥当,然而需要改动的地方很多。没有上版口令无法实现,眼下解决问题可以先通过第三种方法临时解决,等到上版的时候再按照第二种方法处理!  于是SQL等价改写如下:

 

 

SELECT ROWNUM R,F.* 
   FROM (
SELECT E.*
   FROM (SELECT *
           FROM (SELECT /*+ INDEX(C,IDX_CCCCCC_FFFF_INFO) USE_NL(C,PI) LEADING(C) */C.*, PI.CN_NAME
                   FROM CCCCCC_FFFF_INFO C, DDDDD_INFO PI
                  WHERE PI.PARTY_ID = C.PARTY_ID
                  ORDER BY C.TRAN_TIME DESC,C.SYS_SEQ_ID DESC )
          WHERE ROWNUM <= 20
         UNION 
         SELECT *
           FROM (SELECT /*+ INDEX(C,IDX_H_CCCCCC_FFFF_INFO) USE_NL(C,PI) LEADING(C)*/C.*, PI.CN_NAME
                   FROM H_CCCCCC_FFFF_INFO C, DDDDD_INFO PI
                  WHERE PI.PARTY_ID = C.PARTY_ID
                  ORDER BY C.TRAN_TIME DESC,C.SYS_SEQ_ID DESC)
          WHERE ROWNUM <= 20) E  
          ORDER BY E.TRAN_TIME DESC, E.SYS_SEQ_ID DESC ) F
  WHERE ROWNUM <= 20

原SQL是把结果集排序取前20行数据,这里是对两个表分别排序取前20行(合并起来就是40行),再对这40行取前20行 。注意一点:这个改写不是SQL语句上的等价,在业务上也不等价,只是在数据可行的情况下得到的结果等价!!!现在SQL变成了我们所熟悉的分页语句格式,可以利用传统的手段进行优化----建索引

1.分别在CCCCCC_FFFF_INFO和表H_CCCCCC_FFFF_INFO的(TRAN_TIME DESC,SYS_SEQ_ID DESC)列上面建立组合索引

  索引已经排过序,所以这时候只需要通过扫描索引(无论走那种访问路径都行)取20条数据后停止,然后回表20次即可

2.在DDDDD_INFO表(PARTY_ID,CN_NAME)列建组合索引,选择性高的列放在前面,这个表作为NL的被驱动表,

  连接列必须包含在索引里面

 

 

Plan hash value: 2692565397
 
-----------------------------------------------------------------------------------------------------------------------
| Id  | Operation                            | Name                   | Rows  | Bytes |TempSpc| Cost (%CPU)| Time     |
-----------------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                     |                        |    20 | 75520 |       |  1612K  (1)| 05:22:30 |
|*  1 |  COUNT STOPKEY                       |                        |       |       |       |            |          |
|   2 |   VIEW                               |                        |    40 |   147K|       |  1612K  (1)| 05:22:30 |
|*  3 |    SORT ORDER BY STOPKEY             |                        |    40 |   147K|       |  1612K  (1)| 05:22:30 |
|   4 |     VIEW                             |                        |    40 |   147K|       |  1612K  (1)| 05:22:30 |
|   5 |      SORT UNIQUE                     |                        |    40 |   147K|  7106M|  1612K  (1)| 05:22:30 |
|   6 |       UNION-ALL                      |                        |       |       |       |            |          |
|*  7 |        COUNT STOPKEY                 |                        |       |       |       |            |          |
|   8 |         VIEW                         |                        |   909K|  3275M|       |   893K  (1)| 02:58:47 |
|   9 |          NESTED LOOPS                |                        |   909K|   346M|       |   815K  (1)| 02:43:12 |
|  10 |           TABLE ACCESS BY INDEX ROWID| CCCCCC_FFFF_INFO       |   917K|   333M|       |   814K  (1)| 02:42:49 |
|  11 |            INDEX FULL SCAN           | IDX_CCCCCC_FFFF_INFO   |   917K|       |       |  6322   (2)| 00:01:16 |
|* 12 |           INDEX RANGE SCAN           | IDX_DDDDD_INFO         |     1 |    19 |       |     1   (0)| 00:00:01 |
|* 13 |        COUNT STOPKEY                 |                        |       |       |       |            |          |
|  14 |         VIEW                         |                        |   117 |   431K|       |   107   (2)| 00:00:02 |
|  15 |          NESTED LOOPS                |                        |   117 | 45162 |       |   106   (1)| 00:00:02 |
|  16 |           TABLE ACCESS BY INDEX ROWID| H_CCCCCC_FFFF_INFO     |   117 | 42939 |       |   104   (0)| 00:00:02 |
|  17 |            INDEX FULL SCAN           | IDX_H_CCCCCC_FFFF_INFO |   117 |       |       |     1   (0)| 00:00:01 |
|* 18 |           INDEX RANGE SCAN           | IDX_DDDDD_INFO         |     1 |    19 |       |     1   (0)| 00:00:01 |
-----------------------------------------------------------------------------------------------------------------------
 
Predicate Information (identified by operation id):
---------------------------------------------------
 
   1 - filter(ROWNUM<=20)
   3 - filter(ROWNUM<=20)
   7 - filter(ROWNUM<=20)
  12 - access("PI"."PARTY_ID"="C"."PARTY_ID")
  13 - filter(ROWNUM<=20)
  18 - access("PI"."PARTY_ID"="C"."PARTY_ID")

由上面的执行计划我们可以看出指标指标似乎都有所增大。

这里我需要解释一下:cost,time等不能作为优化的关注点,因为本身就是个估算值,没有参考价值

这里造成cost巨大的原因:看第10和11步骤INDEX FULL SCAN估算rows返回90w条,然后回表90w

其实对于分页语句只需要返回20条,然后回表20就行,所以一起看一下下面真实的执行计划:

 

----------------------------------------------------------------------------------------------------------------------------------------------------
| Id  | Operation                            | Name                   | Starts | E-Rows | A-Rows |   A-Time   | Buffers |  OMem |  1Mem | Used-Mem |
----------------------------------------------------------------------------------------------------------------------------------------------------
|*  1 |  COUNT STOPKEY                       |                        |      1 |        |     20 |00:00:00.01 |     136 |       |       |          |
|   2 |   VIEW                               |                        |      1 |     23 |     20 |00:00:00.01 |     136 |       |       |          |
|*  3 |    SORT ORDER BY STOPKEY             |                        |      1 |     23 |     20 |00:00:00.01 |     136 | 18432 | 18432 |16384  (0)|
|   4 |     VIEW                             |                        |      1 |     23 |     20 |00:00:00.01 |     136 |       |       |          |
|   5 |      SORT UNIQUE                     |                        |      1 |     23 |     20 |00:00:00.01 |     136 | 18432 | 18432 |16384  (0)|
|   6 |       UNION-ALL                      |                        |      1 |        |     20 |00:00:00.01 |     136 |       |       |          |
|*  7 |        COUNT STOPKEY                 |                        |      1 |        |     20 |00:00:00.01 |      25 |       |       |          |
|   8 |         VIEW                         |                        |      1 |  20676 |     20 |00:00:00.01 |      25 |       |       |          |
|   9 |          NESTED LOOPS                |                        |      1 |  20676 |     20 |00:00:00.01 |      25 |       |       |          |
|  10 |           TABLE ACCESS BY INDEX ROWID| CCCCCC_FFFF_INFO       |      1 |    914K|     20 |00:00:00.01 |      23 |       |       |          |
|  11 |            INDEX FULL SCAN           | IDX_CCCCCC_FFFF_INFO   |      1 |    914K|     20 |00:00:00.01 |       3 |       |       |          |
|* 12 |           INDEX RANGE SCAN           | IDX_DDDDD_INFO         |     20 |      1 |     20 |00:00:00.01 |       2 |       |       |          |
|* 13 |        COUNT STOPKEY                 |                        |      1 |        |      0 |00:00:00.01 |     111 |       |       |          |
|  14 |         VIEW                         |                        |      1 |      3 |      0 |00:00:00.01 |     111 |       |       |          |
|  15 |          NESTED LOOPS                |                        |      1 |      3 |      0 |00:00:00.01 |     111 |       |       |          |
|  16 |           TABLE ACCESS BY INDEX ROWID| H_CCCCCC_FFFF_INFO     |      1 |    117 |    117 |00:00:00.01 |     109 |       |       |          |
|  17 |            INDEX FULL SCAN           | IDX_H_CCCCCC_FFFF_INFO |      1 |    117 |    117 |00:00:00.01 |       1 |       |       |          |
|* 18 |           INDEX RANGE SCAN           | IDX_DDDDD_INFO         |    117 |      1 |      0 |00:00:00.01 |       2 |       |       |          |
----------------------------------------------------------------------------------------------------------------------------------------------------

逻辑读从4.8w+降到136(其实正常的话逻辑读应该在50左右的。因为测试环境的“特殊原因”导致了这个情况)。响应时间由3s降到0.2S
如果仔细的人可能会看到第17步,这里分页函数应该返回20条数据。为什么会返回117条。这就是上面所说测试环境的“特殊原因”导致的。
这个疑问我这里不做解释,留给大家自己思考,结合上面的SQL和分页函数的特点和原理!


测试环境的这个特殊情况有时在生产环境也可能出现。如果优化分页函数感觉效果不理想甚至响应时间反而变长,就需要特别注意了!

 

 

 

 

 

 

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值