一个通过添加本地分区索引提高SQL性能的案例

通过创建合适的索引将一个原本耗时一分钟的查询优化到仅需两秒,详细介绍了如何识别和解决Oracle SQL性能瓶颈。

今天接到同事求助,说有一个select query,在Oracle上要跑一分多钟,他希望能在5s内出结果,该sql如下:

  1. Select  /*+ parallel(src, 8) */ distinct  
  2.   src.systemname as systemname  
  3.   ,  src.databasename as databasename  
  4.   ,  src.tablename as tablename  
  5.   ,  src.username as username  
  6. from  <strong>meta_dbql_table_usage_exp_hst</strong> src  
  7.  inner <strong>join DR_QRY_LOG_EXP_HST</strong> rl on  
  8.   <strong>src.acctstringdate = rl.acctstringdate  
  9.   and src.queryid = rl.queryid</strong>  
  10.   And Src.Systemname = Rl.Systemname  
  11.   and src.acctstringdate > sysdate - 30  
  12.   And Rl.Acctstringdate > Sysdate - 30  
  13.  inner join  <strong>meta_dr_qry_log_tgt_all_hst </strong>tgt on  
  14.   upper(tgt.systemname) = upper('MOZART')  
  15.   And Upper(tgt.Databasename) = Upper('GDW_TABLES')  
  16.   And Upper(tgt.Tablename) = Upper('SSA_SLNG_LSTG_MTRC_SD')  
  17.   <strong>AND src.acctstringdate = tgt.acctstringdate  
  18.   and rl.statement_id = tgt.statement_id</strong>  
  19.   and rl.systemname = tgt.systemname  
  20.   And Tgt.Acctstringdate > Sysdate - 30  
  21.   And Not(  
  22.     Upper(Tgt.Systemname)=Upper(src.systemname)  
  23.     And  
  24.     Upper(Tgt.Databasename) = Upper(Src.Databasename)  
  25.     And  
  26.     Upper(Tgt.Tablename) = Upper(Src.Tablename)  
  27.     )  
  28.   And   tgt.Systemname is not null  
  29.   And   tgt.Databasename Is Not Null  
  30.   And   tgt.tablename is not null  
  31. ;  

SQL的简单分析

总得来看,这个SQL就是三个表(meta_dbql_table_usage_exp_hst,DR_QRY_LOG_EXP_HST,meta_dr_qry_log_tgt_all_hst)的INNER JOIN,这三个表数据量都在百万级别,且都是分区表(以acctstringdate为分区键),执行计划如下:
  1. ------------------------------------------------------------------------------------------------------------------------  
  2. | Id  | Operation                              | Name                          | Rows  | Bytes | Cost  | Pstart| Pstop |  
  3. ------------------------------------------------------------------------------------------------------------------------  
  4. |   0 | SELECT STATEMENT                       |                               |     1 |   159 |  8654 |       |       |  
  5. |   1 |  PX COORDINATOR                        |                               |       |       |       |       |       |  
  6. |   2 |   PX SEND QC (RANDOM)                  | :TQ10002                      |     1 |   159 |  8654 |       |       |  
  7. |   3 |    SORT UNIQUE                         |                               |     1 |   159 |  8654 |       |       |  
  8. |   4 |     PX RECEIVE                         |                               |     1 |    36 |     3 |       |       |  
  9. |   5 |      PX SEND HASH                      | :TQ10001                      |     1 |    36 |     3 |       |       |  
  10. |*  6 |       TABLE ACCESS BY LOCAL INDEX ROWID| DR_QRY_LOG_EXP_HST            |     1 |    36 |     3 |       |       |  
  11. |   7 |        NESTED LOOPS                    |                               |     1 |   159 |  8633 |       |       |  
  12. |   8 |         NESTED LOOPS                   |                               |  8959 |  1076K|  4900 |       |       |  
  13. |   9 |          BUFFER SORT                   |                               |       |       |       |       |       |  
  14. |  10 |           PX RECEIVE                   |                               |       |       |       |       |       |  
  15. |  11 |            PX SEND BROADCAST           | :TQ10000                      |       |       |       |       |       |  
  16. |  12 |             PARTITION RANGE ITERATOR   |                               |     1 |    56 |  4746 |   KEY |    14 |  
  17. |* 13 |              TABLE ACCESS FULL         | META_DR_QRY_LOG_TGT_ALL_HST   |     1 |    56 |  4746 |   KEY |    14 |  
  18. |  14 |          PX BLOCK ITERATOR             |                               |  8959 |   586K|   154 |   KEY |   KEY |  
  19. |* 15 |           TABLE ACCESS FULL            | META_DBQL_TABLE_USAGE_EXP_HST |  8959 |   586K|   154 |   KEY |   KEY |  
  20. |  16 |         PARTITION RANGE ITERATOR       |                               |     1 |       |     2 |   KEY |   KEY |  
  21. |* 17 |          INDEX RANGE SCAN              | DR_QRY_LOG_EXP_HST_IDX        |     1 |       |     2 |   KEY |   KEY |  
  22. ------------------------------------------------------------------------------------------------------------------------  
  23. Predicate Information (identified by operation id):  
  24. ---------------------------------------------------  
  25.   
  26.    6 - filter("RL"."STATEMENT_ID"="TGT"."STATEMENT_ID" AND "RL"."SYSTEMNAME"="TGT"."SYSTEMNAME" AND "SRC"."SYSTEMNAME"="RL"."SYSTEMNAME")  
  27.   13 - filter(UPPER("TGT"."SYSTEMNAME")='MOZART' AND UPPER("TGT"."DATABASENAME")='GDW_TABLES' AND  
  28.               UPPER("TGT"."TABLENAME")='SSA_SLNG_LSTG_MTRC_SD' AND "TGT"."ACCTSTRINGDATE">SYSDATE@!-30 AND "TGT"."SYSTEMNAME" IS NOT NULL  
  29.               "TGT"."DATABASENAME" IS NOT NULL AND "TGT"."TABLENAME" IS NOT NULL)  
  30.   15 - filter("SRC"."ACCTSTRINGDATE"="TGT"."ACCTSTRINGDATE" AND (UPPER("TGT"."SYSTEMNAME")<>UPPER("SRC"."SYSTEMNAME"OR  
  31.               UPPER("TGT"."DATABASENAME")<>UPPER("SRC"."DATABASENAME"OR UPPER("TGT"."TABLENAME")<>UPPER("SRC"."TABLENAME")) AND  
  32.               "SRC"."ACCTSTRINGDATE">SYSDATE@!-30)  
  33.   17 - access("SRC"."QUERYID"="RL"."QUERYID" AND "SRC"."ACCTSTRINGDATE"="RL"."ACCTSTRINGDATE")  
  34.        filter("RL"."ACCTSTRINGDATE">SYSDATE@!-30)  

定位问题

从上面执行计划中的表连接方式可以知道,这三个表之间进行了两次NESTED LOOP,问题出现在最里层的NESTED LOOP(对两个表都做了TABLE FULL SCAN),因为表都是百万级别的(即时过滤后的数据量也不小),性能问题就出现在内表(即被驱动表)META_DBQL_TABLE_USAGE_EXP_HST做了太多次的全表扫描。如果能把全表扫描转换成索引,则性能可以大幅度提高。

下面是NESTED LOOP的介绍:
嵌套连接把要处理的数据集分为外部循环(驱动数据源)和内部循环(被驱动数据源),外部循环只执行一次,内部循环执行的次数等于外部循环执行返回的数据个数。
这种连接的好处是内存使用非常少。
如果驱动数据源有限,且被驱动表在连接列上有相应的索引,则这种连接方式才是高效的。


下面是这三个表上索引的情况:
  1. SQL> select index_name, table_name from user_indexes where table_name in ('DR_QRY_LOG_EXP_HST',upper('meta_dbql_table_usage_exp_hst'), upper('meta_dr_qry_log_tgt_all_hs  
  2.   
  3. INDEX_NAME                                                   TABLE_NAME  
  4. ------------------------------------------------------------ ------------------------------------------------------------  
  5. META_DR_QRY_LOG_TGT_ALL_IDX                                  META_DR_QRY_LOG_TGT_ALL_HST  
  6. META_DBQL_TUSAGE_EHST_IDX                                    META_DBQL_TABLE_USAGE_EXP_HST  
  7. DR_QRY_LOG_EXP_HST_IDX                                       DR_QRY_LOG_EXP_HST  
  8.   
  9. CREATE INDEX "GV"."META_DR_QRY_LOG_TGT_ALL_IDX" ON "GV"."META_DR_QRY_LOG_TGT_ALL_HST" ("STATEMENT_ID""ACCTSTRINGDATE")  
  10. CREATE INDEX "GV"."META_DBQL_TUSAGE_EHST_IDX" ON "GV"."META_DBQL_TABLE_USAGE_EXP_HST" ("QUERYID""ACCTSTRINGDATE")  
  11. CREATE INDEX "GV"."DR_QRY_LOG_EXP_HST_IDX" ON "GV"."DR_QRY_LOG_EXP_HST" ("QUERYID""ACCTSTRINGDATE")  

这三个索引都是本地分区索引(都包含分区键acctstringdate),很显然,DR_QRY_LOG_EXP_HST表少了个索引,因为它与表meta_dr_qry_log_tgt_all_hst 在statement_id上做join,因此应该在它的statement_id上也创建本地分区索引如下:
  1. create index DR_QRY_LOG_EXP_HST_IDX2 on gv.DR_QRY_LOG_EXP_HST (statement_id,ACCTSTRINGDATE) local;  

性能对比

新的执行计划如下:
  1. ------------------------------------------------------------------------------------------------------------------------  
  2. | Id  | Operation                              | Name                          | Rows  | Bytes | Cost  | Pstart| Pstop |  
  3. ------------------------------------------------------------------------------------------------------------------------  
  4. |   0 | SELECT STATEMENT                       |                               |     1 |   159 |  4838 |       |       |  
  5. |   1 |  SORT UNIQUE                           |                               |     1 |   159 |  4838 |       |       |  
  6. |*  2 |   TABLE ACCESS BY LOCAL INDEX ROWID    | META_DBQL_TABLE_USAGE_EXP_HST |     1 |    67 |     3 |       |       |  
  7. |   3 |    NESTED LOOPS                        |                               |     1 |   159 |  4816 |       |       |  
  8. |   4 |     NESTED LOOPS                       |                               |    18 |  1656 |  4762 |       |       |  
  9. |   5 |      PARTITION RANGE ITERATOR          |                               |     1 |    56 |  4746 |   KEY |    14 |  
  10. |*  6 |       TABLE ACCESS FULL                | META_DR_QRY_LOG_TGT_ALL_HST   |     1 |    56 |  4746 |   KEY |    14 |  
  11. |   7 |      PARTITION RANGE ITERATOR          |                               |    18 |   648 |    16 |   KEY |    14 |  
  12. |*  8 |       TABLE ACCESS BY LOCAL INDEX ROWID| DR_QRY_LOG_EXP_HST            |    18 |   648 |    16 |   KEY |    14 |  
  13. |*  9 |        <strong>INDEX RANGE SCAN                | DR_QRY_LOG_EXP_HST_IDX2</strong>       |    31 |       |    15 |   KEY |    14 |  
  14. |  10 |     PARTITION RANGE ITERATOR           |                               |     1 |       |     2 |   KEY |   KEY |  
  15. |* 11 |      INDEX RANGE SCAN                  | META_DBQL_TUSAGE_EHST_IDX     |     1 |       |     2 |   KEY |   KEY |  
  16. ------------------------------------------------------------------------------------------------------------------------  
  17.   
  18. Predicate Information (identified by operation id):  
  19. ---------------------------------------------------  
  20.   
  21.    2 - filter((UPPER("TGT"."SYSTEMNAME")<>UPPER("SRC"."SYSTEMNAME"OR  
  22.               UPPER("TGT"."DATABASENAME")<>UPPER("SRC"."DATABASENAME"OR UPPER("TGT"."TABLENAME")<>UPPER("SRC"."TABLENAME"))  
  23.               AND "SRC"."SYSTEMNAME"="RL"."SYSTEMNAME")  
  24.    6 - filter(UPPER("TGT"."SYSTEMNAME")='MOZART' AND UPPER("TGT"."DATABASENAME")='GDW_TABLES' AND  
  25.               UPPER("TGT"."TABLENAME")='SSA_SLNG_LSTG_MTRC_SD' AND "TGT"."ACCTSTRINGDATE">SYSDATE@!-30 AND "TGT"."SYSTEMNAME"  
  26.               IS NOT NULL AND "TGT"."DATABASENAME" IS NOT NULL AND "TGT"."TABLENAME" IS NOT NULL)  
  27.    8 - filter("RL"."SYSTEMNAME"="TGT"."SYSTEMNAME")  
  28.    9 - access("RL"."STATEMENT_ID"="TGT"."STATEMENT_ID" AND "RL"."ACCTSTRINGDATE">SYSDATE@!-30 AND  
  29.               "RL"."ACCTSTRINGDATE" IS NOT NULL)  
  30.   11 - access("SRC"."QUERYID"="RL"."QUERYID" AND "SRC"."ACCTSTRINGDATE"="RL"."ACCTSTRINGDATE")  
  31.        filter("SRC"."ACCTSTRINGDATE"="TGT"."ACCTSTRINGDATE" AND "SRC"."ACCTSTRINGDATE">SYSDATE@!-30)  

从新的的执行计划可以看出,它的第一个NESTED LOOP果然用了最新创建的索引。
下面是执行时间:
[plain]  view plain copy print ?
  1. 已用时间:  00: 00: 02.16  

两秒种搞定,远远超出他期望的5s :)

方法总结

NESTED LOOP高效的条件: 驱动数据源有限,且被驱动表在连接列上有相应的索引。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值