記一個SQL優化案例

weixin_33766168發表於2017-11-14

某省電信在做批扣(批銷)時,出現嚴重的效能問題,發現下面這一條SQL效能非常低下:

SELECT A.ACCT_BALANCE_ID,
       A.BALANCE_TYPE_ID,
       A.ACCT_ID,
       NVL(A.SERV_ID, -1) SERV_ID,
       NVL(A.ITEM_GROUP_ID, -1) ITEM_GROUP_ID,
       A.OBJECT_TYPE_ID,
       F.PRIORITY,
       A.BALANCE,
       NVL(A.CYCLE_UPPER, -1) CYCLE_UPPER,
       NVL(A.CYCLE_LOWER, -1) CYCLE_LOWER,
       NVL(A.CYCLE_UPPER_TYPE, ’ ’) CYCLE_UPPER_TYPE,
       NVL(A.CYCLE_LOWER_TYPE, ’ ’) CYCLE_LOWER_TYPE,
       B.ADJUST_FLAG ADJUST_FLAG,
       B.ALLOW_TRANS ALLOW_TRANS,
       B.CORPUS_FLAG,
       NVL(TO_CHAR(A.EFF_DATE, ’YYYYMMDDHH24MISS’), ’ ’) EFF_DATE,
       NVL(TO_CHAR(A.EXP_DATE, ’YYYYMMDDHH24MISS’), ’ ’) EXP_DATE,
       A.STATE,
       TO_CHAR(A.STATE_DATE, ’YYYYMMDDHH24MISS’) STATE_DATE,
       B.BALANCE_TYPE_NAME,
       NVL(C.ACCT_NAME, ’ ’) ACCT_NAME,
       NVL(D.ACC_NBR, ’ ’) SERV_NAME,
       NVL(E.ITEM_GROUP_NAME, ’ ’) ITEM_GROUP_NAME
  FROM (SELECT ACCT_BALANCE_ID,
               BALANCE_TYPE_ID,
               ACCT_ID,
               NVL(SERV_ID, -1) SERV_ID,
               NVL(ITEM_GROUP_ID, -1) ITEM_GROUP_ID,
               OBJECT_TYPE_ID,
               BALANCE,
               NVL(CYCLE_UPPER, -1) CYCLE_UPPER,
               NVL(CYCLE_LOWER, -1) CYCLE_LOWER,
               NVL(CYCLE_UPPER_TYPE, ’ ’) CYCLE_UPPER_TYPE,
               NVL(CYCLE_LOWER_TYPE, ’ ’) CYCLE_LOWER_TYPE,
               EFF_DATE,
               EXP_DATE,
               STATE,
               STATE_DATE,
               0 SHARE_RULE_PRIORITY
          FROM ACCT_BALANCE
         WHERE ACCT_ID = :LACCTID
           AND BALANCE > 0
           AND BALANCE_TYPE_ID != 1
           AND STATE = ’10A’
        UNION
        SELECT A1.ACCT_BALANCE_ID,
               A1.BALANCE_TYPE_ID,
               B1.ACCT_ID,
               NVL(B1.SERV_ID, -1) SERV_ID,
               NVL(B1.ITEM_GROUP_ID, -1) ITEM_GROUP_ID,
               A1.OBJECT_TYPE_ID,
               A1.BALANCE,
               NVL(B1.UPPER_AMOUNT, -1) CYCLE_UPPER,
               NVL(B1.LOWER_AMOUNT, -1) CYCLE_LOWER,
               NVL(A1.CYCLE_UPPER_TYPE, ’ ’) CYCLE_UPPER_TYPE,
               NVL(A1.CYCLE_LOWER_TYPE, ’ ’) CYCLE_LOWER_TYPE,
               A1.EFF_DATE,
               A1.EXP_DATE,
               A1.STATE,
               A1.STATE_DATE,
               B1.PRIORITY SHARE_RULE_PRIORITY
          FROM ACCT_BALANCE A1, BALANCE_SHARE_RULE B1
         WHERE A1.ACCT_BALANCE_ID = B1.ACCT_BALANCE_ID
           AND B1.ACCT_ID = :LACCTID
           AND A1.BALANCE > 0
           AND A1.BALANCE_TYPE_ID != 1
           AND A1.STATE = ’10A’
           AND NVL(B1.EFF_DATE, SYSDATE) < = SYSDATE
           AND NVL(B1.EXP_DATE, SYSDATE) >= SYSDATE) A,
       BALANCE_TYPE B,
       ACCT C,
       SERV D,
       A_BALANCE_ITEM_GROUP E,
       A_BALANCE_OBJECT_TYPE F
 WHERE A.BALANCE_TYPE_ID = B.BALANCE_TYPE_ID
   AND A.OBJECT_TYPE_ID = F.OBJECT_TYPE_ID
   AND A.ACCT_ID = C.ACCT_ID
   AND A.SERV_ID = D.SERV_ID(+)
   AND A.ITEM_GROUP_ID = E.ITEM_GROUP_ID(+)
 ORDER BY F.PRIORITY,
          B.PRIORITY,
          A.SHARE_RULE_PRIORITY ASC,
          A.EXP_DATE ASC,
          A.EFF_DATE ASC,
          A.BALANCE ASC

檢視執行計劃:

SQL> select * from table(dbms_xplan.display);

PLAN_TABLE_OUTPUT
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------

------------------------------------------------------------------------------------------------------------------------------------
| Id  | Operation                                    |  Name                       | Rows  | Bytes |TempSpc| Cost  | Pstart| Pstop |
------------------------------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                             |                             |  2723G|   696T|       |  7776M|       |       |
|   1 |  SORT ORDER BY                               |                             |  2723G|   696T|  1503T|  7776M|       |       |
|*  2 |   HASH JOIN                                  |                             |  2723G|   696T|    59M| 39355 |       |       |
|*  3 |    HASH JOIN                                 |                             |   228K|    56M|       | 23918 |       |       |
|   4 |     TABLE ACCESS FULL                        | BALANCE_TYPE                |     8 |   184 |       |     7 |       |       |
|*  5 |     HASH JOIN                                |                             |   228K|    51M|       | 23907 |       |       |
|   6 |      TABLE ACCESS FULL                       | A_BALANCE_OBJECT_TYPE       |     4 |    16 |       |     7 |       |       |
|*  7 |      HASH JOIN OUTER                         |                             |   228K|    50M|    43M| 23896 |       |       |
|*  8 |       HASH JOIN OUTER                        |                             |   228K|    40M|    38M| 23199 |       |       |
|   9 |        VIEW                                  |                             |   228K|    36M|       |  2043 |       |       |
|  10 |         SORT UNIQUE                          |                             |   228K|    11M|    38M|  2043 |       |       |
|  11 |          UNION-ALL                           |                             |       |       |       |       |       |       |
|* 12 |           TABLE ACCESS BY GLOBAL INDEX ROWID | ACCT_BALANCE                |   228K|    11M|       |    50 | ROWID | ROW L |
|* 13 |            INDEX RANGE SCAN                  | IDX_ACCT_BALANCE_ACCT_ID42  |   121K|       |       |     3 |       |       |
|  14 |           NESTED LOOPS                       |                             |     1 |   146 |       |     4 |       |       |
|* 15 |            TABLE ACCESS FULL                 | BALANCE_SHARE_RULE          |     1 |   109 |       |     2 |       |       |
|* 16 |            TABLE ACCESS BY GLOBAL INDEX ROWID| ACCT_BALANCE                |     1 |    37 |       |     2 | ROWID | ROW L |
|* 17 |             INDEX UNIQUE SCAN                | PK_P_ACCT_BALANCE2          |     1 |       |       |     1 |       |       |
|  18 |        PARTITION RANGE ALL                   |                             |       |       |       |       |     1 |    63 |
|  19 |         TABLE ACCESS FULL                    | SERV                        |    12M|   258M|       | 14070 |     1 |    63 |
|  20 |       TABLE ACCESS FULL                      | A_BALANCE_ITEM_GROUP        |   244 | 11224 |       |     7 |       |       |
|  21 |    PARTITION RANGE ALL                       |                             |       |       |       |       |     1 |    63 |
|  22 |     TABLE ACCESS FULL                        | ACCT                        |    11M|   239M|       |  8505 |     1 |    63 |
------------------------------------------------------------------------------------------------------------------------------------

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

2 - access(”A”.”ACCT_ID”=”C”.”ACCT_ID”)
3 - access(”A”.”BALANCE_TYPE_ID”=”B”.”BALANCE_TYPE_ID”)
5 - access(”A”.”OBJECT_TYPE_ID”=”F”.”OBJECT_TYPE_ID”)
7 - access(”A”.”ITEM_GROUP_ID”=”E”.”ITEM_GROUP_ID”(+))
8 - access(”A”.”SERV_ID”=”D”.”SERV_ID”(+))
12 - filter(”ACCT_BALANCE”.”BALANCE”>0 AND “ACCT_BALANCE”.”BALANCE_TYPE_ID”<>1 AND “ACCT_BALANCE”.”STATE”=’10A’)
13 - access(”ACCT_BALANCE”.”ACCT_ID”=TO_NUMBER(:Z))
15 - filter(”B1″.”ACCT_ID”=TO_NUMBER(:Z) AND NVL(”B1″.”EFF_DATE”,SYSDATE@!)< =SYSDATE@! AND
NVL("B1"."EXP_DATE",SYSDATE@!)>=SYSDATE@!)
16 - filter(”A1″.”BALANCE”>0 AND “A1″.”BALANCE_TYPE_ID”<>1 AND “A1″.”STATE”=’10A’)
17 - access(”A1″.”ACCT_BALANCE_ID”=”B1″.”ACCT_BALANCE_ID”)


從執行計劃來看,Oracle評估出來的COST是相當的驚人(7776M),而返回的行數是2723G。看起來應該是統計資訊不準確導致了選擇了錯誤的執行計劃。

對於一個SQL,如果其執行計劃錯誤地採用於full table scan和hash join,則一般是由於評估表訪問返回的資料行數過多,使其高估了成本。反之,如果是錯誤地採用了索引掃描,則一般是由於評估表訪問返回的資料行數太少,使其低估了成本。

我們來分析這個執行計劃。首先檢視訪問表時,Oracle評估返回的行的數目是否有偏大的情況。我們發現看以下的幾行,返回的結果集行數很高:

|* 12 |           TABLE ACCESS BY GLOBAL INDEX ROWID | ACCT_BALANCE                |   228K|    11M|       |    50 | ROWID | ROW L |
|* 13 |            INDEX RANGE SCAN                  | IDX_ACCT_BALANCE_ACCT_ID42  |   121K|       |       |     3 |       |       |
|  19 |         TABLE ACCESS FULL                    | SERV                        |    12M|   258M|       | 14070 |     1 |    63 |
|  22 |     TABLE ACCESS FULL                        | ACCT                        |    11M|   239M|       |  8505 |     1 |    63 |

最值得懷疑的首先是第12行和第13行。第13行是一個索引範圍掃描,返回的結果集行數居然有121K行,而這個訪問條件只是 “access(”ACCT_BALANCE”.”ACCT_ID”=TO_NUMBER(:Z))”,從業務上來看,看起來這條SQL是查詢某個帳戶的 餘額及其他資訊的。輸入的條件為ACCT_ID,而一般來說,帳戶餘額表中,一個ACCT_ID的對應的行,只有很少的行(10行以下),而不會返回這麼 多行。

IDX_ACCT_BALANCE_ACCT_ID42這個索引,是一個在ACCT_BALANCE表(這是個分割槽表)的ACCT_ID上的全域性普 通索引(GLOBAL NORMAL INDEX)。檢查統計資訊,這個表是當天才收集的,ACCT_ID這個欄位沒有任何統計資訊(已經被刪除)。同時索引中的統計資訊如下:

Index                      Tree Leaf       Distinct         Number Leaf Blocks Data Blocks      Cluster
Name            Unique    Level Blks           Keys        of Rows     Per Key     Per Key       Factor
--------------- --------- ----- ---- -------------- -------------- ----------- ----------- ------------
IDX_ACCT_BALANC NONUNIQUE     2 ####        580,079     30,742,660           1          47   27,447,720
E_ACCT_ID42                                                                                            

看起來索引的Distinct Keys值太小,才580,079。索引的總行數為30,742,660。重新分析該索引後,Distinct Keys的值才是正確的值:10,540,493。
再看看ACCT_BALANCE表中的ACCT_ID欄位,由於沒有統計資訊,那麼Oracle會使用一個預設的選擇率(對於表的單個欄位條件過濾,一般 是1%)。對於這樣一個3000多萬行的表來說,預設的選擇率造成評估的返回行數過高。由於正是出帳期間,重新分析這樣一個表,影響是非常大的,而這個表 之前剛分析過。我們這裡可以使用DBMS_STATS.SET_COLUMN_STATS過程手工設定統計資訊:

SQL> exec dbms_stats.set_column_stats(ownname=>user,tabname=>’ACCT_BALANCE’,colname=>’ACCT_ID’,
NULLCNT=>0,DENSITY=>1/10540393,DISTCNT=>10540393);

這裡的統計資訊來自於ACCT_ID欄位上的索引(ACCT_ID欄位定義為NOT NULL,所以比較方便地手工計算出這些統計資訊)。

我們再次檢視執行計劃:

PLAN_TABLE_OUTPUT
------------------------------------------------------------------------------------------------------------------------------------------------------

------------------------------------------------------------------------------------------------------------------------------------
| Id  | Operation                                    |  Name                       | Rows  | Bytes |TempSpc| Cost  | Pstart| Pstop |
------------------------------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                             |                             |    35M|  9603M|       |  1472K|       |       |
|   1 |  SORT ORDER BY                               |                             |    35M|  9603M|    20G|  1472K|       |       |
|   2 |   NESTED LOOPS                               |                             |    35M|  9603M|       |    62 |       |       |
|   3 |    NESTED LOOPS OUTER                        |                             |     3 |   780 |       |    56 |       |       |
|   4 |     NESTED LOOPS OUTER                       |                             |     3 |   717 |       |    50 |       |       |
|   5 |      NESTED LOOPS                            |                             |     3 |   579 |       |    47 |       |       |
|   6 |       NESTED LOOPS                           |                             |     3 |   510 |       |    44 |       |       |
|   7 |        VIEW                                  |                             |     3 |   498 |       |    42 |       |       |
|   8 |         SORT UNIQUE                          |                             |     3 |   248 |       |    42 |       |       |
|   9 |          UNION-ALL                           |                             |       |       |       |       |       |       |
|* 10 |           TABLE ACCESS BY GLOBAL INDEX ROWID | ACCT_BALANCE                |     2 |   102 |       |     6 | ROWID | ROW L |
|* 11 |            INDEX RANGE SCAN                  | IDX_ACCT_BALANCE_ACCT_ID42  |     3 |       |       |     3 |       |       |
|  12 |           NESTED LOOPS                       |                             |     1 |   146 |       |     4 |       |       |
|* 13 |            TABLE ACCESS FULL                 | BALANCE_SHARE_RULE          |     1 |   109 |       |     2 |       |       |
|* 14 |            TABLE ACCESS BY GLOBAL INDEX ROWID| ACCT_BALANCE                |     1 |    37 |       |     2 | ROWID | ROW L |
|* 15 |             INDEX UNIQUE SCAN                | PK_P_ACCT_BALANCE2          |     1 |       |       |     1 |       |       |
|  16 |        TABLE ACCESS BY INDEX ROWID           | A_BALANCE_OBJECT_TYPE       |     1 |     4 |       |     1 |       |       |
|* 17 |         INDEX UNIQUE SCAN                    | PK_A_BALANCE_OBJECT_TYPE    |     1 |       |       |       |       |       |
|  18 |       TABLE ACCESS BY INDEX ROWID            | BALANCE_TYPE                |     1 |    23 |       |     1 |       |       |
|* 19 |        INDEX UNIQUE SCAN                     | PK_BALANCE_TYPE             |     1 |       |       |       |       |       |
|  20 |      TABLE ACCESS BY INDEX ROWID             | A_BALANCE_ITEM_GROUP        |     1 |    46 |       |     1 |       |       |
|* 21 |       INDEX UNIQUE SCAN                      | PK_ITEM_GROUP_ID_HN         |     1 |       |       |       |       |       |
|  22 |     TABLE ACCESS BY GLOBAL INDEX ROWID       | SERV                        |     1 |    21 |       |     2 | ROWID | ROW L |
|* 23 |      INDEX UNIQUE SCAN                       | PK_SERV                     |     1 |       |       |     1 |       |       |
|  24 |    TABLE ACCESS BY GLOBAL INDEX ROWID        | ACCT                        |    11M|   239M|       |     2 | ROWID | ROW L |
|* 25 |     INDEX UNIQUE SCAN                        | PK_ACCT                     |     1 |       |       |     1 |       |       |
------------------------------------------------------------------------------------------------------------------------------------

現在可以看到,執行計劃已經是改善了很多,只有唯一的一個全表掃描--BALANCE_SHARE_RULE,不過那個表的行數為0。

這個時候,客戶反映系統正常了。雖然Oracle在ACCT表上評估返回的行數仍然很高,但是從系統故障處理這個角度上講目的已經達到了。接下來就是處理這個ACCT表,類似的問題,類似的過程,這裡不再細述。

總結:

此次效能問題的處理,從接到客戶電話,到撥號連線,到最終處理完成,不到1小時。個人認為,這個處理速度還是比較快速的。做一個如下的總結:

  • 瞭解SQL是幹什麼的(也就是理解SQL的業務),非常有助於調優。
  • 調優需要善於與客戶和開發溝通。
  • 很多時候,現在ORACLE的優化器已經非常智慧和完善,SQL效能問題是由於優化統計資訊不準確。使用hint那個是治標不治 本。只有在非常複雜的SQL(比如有非常多的內聯檢視,子查詢等等),才應該考慮HINT。這只是個人觀點。當然還會有其他需要使用HINT才能解決的地 方。
  • 善用指令碼,這個案例中,我再次使用了sosi.sql這個指令碼來檢視錶的優化統計資訊,能夠大大節約時間,同時檢視的優化統計資訊非常完整。
  • 善用DBMS_STATS,在這個案例中,使用set_column_stats,大大節省了收集優化統計資訊的時間。




本文轉自 vfast_chenxy 51CTO部落格,原文連結:http://blog.51cto.com/chenxy/896566,如需轉載請自行聯絡原作者

相關文章