甲方某一个客户反应一个报表页面查询点击之后需要等很久(半分钟)才能显示数据,点击页面生成的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和分页函数的特点和原理!
测试环境的这个特殊情况有时在生产环境也可能出现。如果优化分页函数感觉效果不理想甚至响应时间反而变长,就需要特别注意了!