閱讀257 返回首頁    go 阿裏雲 go 技術社區[雲棲]


性能為王:SQL標量子查詢的優化案例分析

640?wx_fmt=jpeg&tp=webp&wxfrom=5&wx_lazy
黃廷忠(網名:認真就輸)

雲和恩墨技術專家

個人博客:https://www.htz.pw/

本篇整理內容是黃廷忠在“雲和恩墨大講堂”微信分享中的講解案例,SQL優化及SQL審核,是從源頭解決性能問題的根本手段,無論是開發人員還是DBA,都應當持續深入的學習SQL開發技能,從而為解決性能問題打下根基。


本篇為係列案例之一:標量子查詢優化

 

以下案例來自於某省電信係統EDW性能優化實踐,數據庫版本為11.2.0.3,運行在ORACLE Exadata一體機上,是個典型的OLAP環境,表上無索引,表無統計信息

SQL性能問題診斷


SQL的基本邏輯如下: 

SELECT OFFER_SERV_SUM AS N37364,

       LOCAL_CODE LOCAL_CODE,

       AREA_ID AREA_ID,

       DVLP_AREA_ID MG_AREA_ID,

       DVLP_ORG_ID ORG_ID,

       CASE

         WHEN OFFER_SPEC_ID IN (SELECT LOCAL_ITEM_CODE

                                  FROM PU_META_DIM.CODE_ITEM

                                 WHERE PROV_TYPE_ID = 49

                                   AND PROV_ITEM_ID = 64) THEN

          1 ELSE 0

       END || CASE

         WHEN TO_CHAR(OFFER_SPEC_ID) IN

              (SELECT LOCAL_ITEM_CODE

                 FROM PU_META_DIM.CODE_ITEM A

                WHERE PROV_TYPE_ID = 49

                  AND PROV_ITEM_ID IN (85, 86, 68, 69, 70, 71)) THEN

          1 ELSE 0

       END || CASE

         WHEN TO_CHAR(OFFER_SPEC_ID) IN

              (SELECT OFFER_SPEC_ID

                 FROM PU_META_DIM.D_DRAGON_PLAN

                WHERE BT_CODE = 1

                  AND (UPPER(NAME) LIKE'%4S%'ORNAMELIKE'%??%')) THEN

          1 ELSE 0

       END || CASE

         WHEN TO_CHAR(OFFER_SPEC_ID) IN

              (SELECT LOCAL_ITEM_CODE

                 FROM PU_META_DIM.CODE_ITEM

                WHERE PROV_TYPE_ID = 49

                  AND PROV_ITEM_ID = 64) THEN

         1 ELSE 0

       END || CASE

         WHEN TO_CHAR(OFFER_SPEC_ID) IN

              (SELECT OFFER_SPEC_ID 

                 FROM PU_META_DIM.TY_SUIT_CFG) THEN

         1 ELSE 0

       END || CASE

         WHEN TO_CHAR(OFFER_SPEC_ID) IN

              (SELECT OFFER_SPEC_ID

                 FROM PU_META_DIM.D_DRAGON_PLAN

                WHERE BT_CODE = 2

                  AND (UPPER(NAME) LIKE'%4S%'ORNAMELIKE'%??%')) THEN

          1 ELSE 0

       END | C_ALL

  FROM PU_BASE_IND.DM_SP_SUB_OFFER_SERV_D_201407 

  PARTITION(P20140727) A

 WHERE LOCAL_CODE = '028'

 

這個SQL一共格式化出來有600多行,由於篇幅的限製,這裏把其它一些無用的列的信息去掉。下麵來查詢一下SQL的執行計劃:

640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=

從上麵的關鍵字部分STORAGE FULL FIRST ROWS這部分,我們也可以看到是一個Exadata一體機的環境。 

SQL的執行計劃很簡單,一共隻有10行。FROM後對一個分區表的一個子分區執行全分區掃描。

 

下麵來看看這個SQL每次執行消耗的物理讀與邏輯讀。

640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=


這裏需要關注幾點:

1,  每次執行消耗的物理讀(diskpre exec)

2,  每次執行平均消耗的邏輯讀(getpre exec)

3,  每次執行平均返回的行數(rowspre exec)


這個腳本的輸出我們還需要計算一下

1,  每次執行SQL,返回的每行平均消耗的邏輯讀338280770/4302704=78.62,大概邏輯讀78才能換回一行。

2,  每次執行SQL,返回的每行平均消耗的物理讀22610/4302704=0.005 

從上麵幾點,大概知道這個SQL存在性能問題。


基礎信息分析

PU_BASE_IND.DM_SP_SUB_OFFER_SERV_D_201407是個分區表,下麵查詢一下表分區的信息:

640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=

 

這裏可以看到表是一個RANGE-LIST的分區表。下麵查詢SQL關聯的表的大小: 

640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=

由於是分區表,所有這裏需要去查詢訪問的分區的大小:

640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=


這裏看到訪問的分區隻有84M,加上code_item,ty_suit_cfg,d_dragon_plan表一共才4288+84=4372M,等於 =559616 個BLOCK。

 

下麵我們考慮一種極端的條件下,SQL訪問的幾張表都走全表掃描,並且走HASH連接。那麼此時物理讀加邏輯讀應該接近 559,616 (這裏不考慮TEMP等消耗,不考慮事務一致性等原因,隻考慮表的大小),但是整個SQL消耗的物理都為22610,邏輯讀是:338,280,770。遠遠大於SQL訪問的表占用的物理大小。所以初步判斷在執行計劃中存在某個對象被輪詢。

 

下麵查詢訪問的分區的行數,這裏由於是OLAP係統,無統計信息,隻能手動運行SQL來查詢,如果有統計信息,可以大概根據統計信息來計算,雖然不是很準確,但是我覺得足夠我們判斷SQL性能了,何況CBO都是基於統計信息的。

 

640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=

這裏看到028返回了400W行的數據。


問題定位與SQL改寫 

通過上麵的信息,可以知道SQL變慢的原因 :

由於PU_BASE_IND.DM_SP_SUB_OFFER_SERV_D_201407 PARTITION(P20140727) 返回一行,就需要去執行所有的標量子查詢,雖然在一體機環境這裏有first rows,但是所有的標量子查詢被輪詢的次數達到了4,302,704次,並且子查詢的表走的全表掃描,所以出現了邏輯讀很高。

 

邏輯讀比物理讀性能好,並且邏輯讀消耗的時間很短,但是過高的邏輯讀會帶來CPU使用率的增加,RAC環境會導致過多的GC等待,還有可能會影響後來的一些TX,INDEX ITL等等待事件的出現,前不久就曾經遇到一個邏輯讀導致GC等待,又引起了TX,INDEX ITL,BBW等待事件,沒有多久,業務連接池就滿了,最後整個業務受影響。

 

為了減少標量子查詢被輪詢的次數,這裏可以把標量子查詢改為外連接。

為了減少SQL長度,下麵是去掉SELECT中一些不需要的部分,我們手動測試一下SQL改為外連接與原SQL執行SQL的差異:

SELECT OFFER_SERV_SUM AS N37364,

      LOCAL_CODE LOCAL_CODE,

      AREA_ID AREA_ID,

      DVLP_AREA_ID MG_AREA_ID,

      DVLP_ORG_ID ORG_ID,

         CASE WHEN a.OFFER_SPEC_ID = b.LOCAL_ITEM_CODE THEN 1 ELSE 0 END

      || CASE

            WHEN TO_CHAR (a.OFFER_SPEC_ID) = e.LOCAL_ITEM_CODE THEN 1

            ELSE 0

         END

      || CASE

            WHEN     TO_CHAR (a.OFFER_SPEC_ID)= c.offer_spec_id

                  AND c.bt_code = 1

            THEN

                1

            ELSE

                0

         END

      || CASE WHEN a.OFFER_SPEC_ID = b.LOCAL_ITEM_CODE THEN 1 ELSE 0 END

      || CASE

            WHEN TO_CHAR (a.OFFER_SPEC_ID) = d.OFFER_SPEC_ID THEN 1

            ELSE 0

         END

      || CASE

            WHEN     TO_CHAR (a.OFFER_SPEC_ID)= c.offer_spec_id

                  AND c.bt_code = 2

            THEN

                1

            ELSE

                0

         END

         C_ALL

 FROM PU_BASE_IND.DM_SP_SUB_OFFER_SERV_D_201407 PARTITION (P20140727) A,

      (SELECT DISTINCT LOCAL_ITEM_CODE

         FROM PU_META_DIM.CODE_ITEM

        WHERE PROV_TYPE_ID = 49 AND PROV_ITEM_ID = 64) b,

      (SELECT DISTINCT OFFER_SPEC_ID, bt_code

         FROM PU_META_DIM.D_DRAGON_PLAN

        WHERE     BT_CODE IN (1, 2)

               AND (UPPER (NAME) LIKE '%4S%' ORNAME LIKE '%??%')) c,

      (SELECT DISTINCT OFFER_SPEC_ID FROM PU_META_DIM.TY_SUIT_CFG) d,

      (SELECT DISTINCT LOCAL_ITEM_CODE

         FROM PU_META_DIM.CODE_ITEM A

        WHERE PROV_TYPE_ID = 49 AND PROV_ITEM_ID IN (85, 86, 68, 69, 70, 71)) e

 WHERE    LOCAL_CODE = '028'

      AND a.OFFER_SPEC_ID = b.LOCAL_ITEM_CODE(+)

      AND TO_CHAR (a.OFFER_SPEC_ID) = c.OFFER_SPEC_ID(+)

      AND TO_CHAR (a.OFFER_SPEC_ID) = d.OFFER_SPEC_ID(+)

      AND TO_CHAR (a.OFFER_SPEC_ID) = e.LOCAL_ITEM_CODE(+)

 

執行計劃及統計信息:

640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=

這裏可以看到所有的表都是通過HASH 來做UNIQUE


640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=

 這裏看到邏輯讀是303475,行返回是4,302,704,不知道大家是否還記得在之前我們獲取SQL的統計信息的時候,看到SQL每次執行平均換回的行數是4,302,704。 

640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=

兩個值是一樣的,說明我們在此條SQL改寫後是等價的。 

這裏用到了”此條”,因為如果在連接列有一些空值的情況下得到的結果可以不一樣,大家可以測試一下。


性能優化效果對比

下麵看看原SQL去掉SELECT不需要的部分執行的信息

SELECT OFFER_SERV_SUM AS N37364,

      LOCAL_CODE LOCAL_CODE,

      AREA_ID AREA_ID,

      DVLP_AREA_ID MG_AREA_ID,

       DVLP_ORG_ID ORG_ID,

        CASE

            WHEN OFFER_SPEC_ID IN

                     (SELECT LOCAL_ITEM_CODE

                        FROMPU_META_DIM.CODE_ITEM

                       WHERE PROV_TYPE_ID = 49AND PROV_ITEM_ID = 64)

            THEN

                1

            ELSE

                0

         END

      || CASE

            WHEN TO_CHAR (OFFER_SPEC_ID) IN

                     (SELECT LOCAL_ITEM_CODE

                        FROMPU_META_DIM.CODE_ITEM A

                       WHERE     PROV_TYPE_ID = 49

                             AND PROV_ITEM_IDIN (85, 86, 68, 69, 70, 71))

            THEN

                1

            ELSE

                0

         END

      || CASE

            WHEN TO_CHAR (OFFER_SPEC_ID) IN

                    (SELECT OFFER_SPEC_ID

                        FROMPU_META_DIM.D_DRAGON_PLAN

                       WHERE     BT_CODE = 1

                             AND (   UPPER (NAME) LIKE '%4S%'

                                  OR NAME LIKE'%??%'))

            THEN

                1

            ELSE

                0

         END

      || CASE

            WHEN TO_CHAR (OFFER_SPEC_ID) IN

                     (SELECT LOCAL_ITEM_CODE

                        FROMPU_META_DIM.CODE_ITEM

                       WHERE PROV_TYPE_ID = 49AND PROV_ITEM_ID = 64)

            THEN

                1

            ELSE

                0

         END

      || CASE

            WHEN TO_CHAR (OFFER_SPEC_ID) IN

                     (SELECT OFFER_SPEC_ID FROMPU_META_DIM.TY_SUIT_CFG)

            THEN

                1

            ELSE

                0

         END

      || CASE

            WHEN TO_CHAR (OFFER_SPEC_ID) IN

                     (SELECT OFFER_SPEC_ID

                        FROM PU_META_DIM.D_DRAGON_PLAN

                       WHERE     BT_CODE = 2

                             AND (   UPPER (NAME) LIKE '%4S%'

                                  OR NAME LIKE'%??%'))

            THEN

                1

            ELSE

                0

         END

         C_ALL

 FROM PU_BASE_IND.DM_SP_SUB_OFFER_SERV_D_201407 PARTITION (P20140727) A

 WHERE LOCAL_CODE = '028'

 

這條語句,運行了30分鍾,都沒有任何行返回:

640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=

可以看到執行了27分鍾,才處理1908315,接近1/2的結果集行數。如果要處理完,估計需要60分鍾以上。


總結 

下麵來總結一下此案例:

1. 在標量子查詢中,當主查詢返回一行數據時,所有的標量子查詢就要執行一次,如果在連接列有索引時,標量子查詢在主表返回的行很少的情況下,對性能影響不大,常常出現在OLTP環境,並且連接列一般都有索引;如果在OLAP環境中,看到標量子查詢千萬要小心,通常,主表返回的行很多,並且子查詢中的表通常在連接列上麵無索引,導致性能很低下,本案例就是這種情況; 

2. 平均每次執行時消耗的邏輯讀、物理讀,返回的行平均消耗的邏輯讀、物理讀,平均返回的行數等信息可以用於初步判斷SQL是否存在性能問題

 

遇到這種標量子查詢,就得修改SQL,也就意味著業務需要修改代碼,像電信運營商這種環境可能要好一點,有專門的開發團隊,但是估計提交修改申請、開發修改、業務測試上線,差不多也要1到2個月的時間,如果在一些小的環境,估計開發商都找不到了,就跟談不上改業務了。

12c新特性改進

對於類似以上的情況,在Oracle Database 12C中,優化器已經可以自動實現等價改寫,但是需要注意的在12.1.0.2版本中有BUG,可能導致結果集不準確。

 

在12C中,標量子查詢自動改寫的功能由隱含參數 _optimizer_unnest_scalar_sq 控製,默認是TRUE,意味著開啟,如果遇到BUG或者性能問題,可以更改為FALSE。


文章轉自數據和雲公眾號,原文鏈接

最後更新:2017-07-18 20:35:56

  上一篇:go  SQL性能突然降低引起的業務辦理緩慢案例一則
  下一篇:go  Windows開源Web服務器性能和壓力測試工具