1. 程式人生 > >Oracle CBO選錯執行計劃的一種場景

Oracle CBO選錯執行計劃的一種場景

測試人員做應用效能測試,反饋有一條SQL語句,之前執行非常快,現在執行時間,明顯慢了。

 

SQL語句非常簡單,根據日期欄位,檢索當日表中記錄數,

select count(*) from user.tbl where cki_date = to_date('2017-10-10','yyyy-mm-dd');

 

我們執行一次10053,

SQL> alter sessionset events '10053 trace name context forever,level 1'; 

Session altered.

 

SQL> explain planfor select count(*) from user.tbl where cki_date =to_date('2017-10-10','yyyy-mm-dd');

Explained.

 

SQL> alter sessionset events '10053 trace name context off'; 

Session altered.

 

看下對應的trace檔案,

***************************************

BASE STATISTICALINFORMATION

***********************

Table Stats::

  Table: TBL  Alias: TBL

    #Rows: 0 #Blks:  1  AvgRowLen: 0.00  ChainCnt:  0.00

Index Stats::

  Index: AIRDATEINDEX  Col#: 1 3

    LVLS: 3 #LB: 121774  #DK: 25329  LB/K: 4.00 DB/K: 49.00  CLUF: 1258940.00

  Index: DATEINDEX  Col#: 3

    LVLS: 2 #LB: 102501  #DK: 5038  LB/K: 20.00 DB/K: 145.00  CLUF: 730978.00

  Index: DESDATEINDEX  Col#: 5 3

    LVLS: 3 #LB: 131196  #DK: 1216564  LB/K: 1.00 DB/K: 11.00  CLUF: 14149049.00

  Index: ORGDATEINDEX  Col#: 4 3

    LVLS: 3 #LB: 130168  #DK: 1027146  LB/K: 1.00 DB/K: 13.00  CLUF: 13373950.00

Access path analysisfor TBL

***************************************

SINGLE TABLE ACCESSPATH

  Single Table Cardinality Estimation forTBL[TBL]

  Column (#3): CKI_DATE(

    AvgLen: 7 NDV: 0 Nulls: 0 Density: 0.000000

  Table: TBL  Alias: TBL

    Card: Original: 0.000000  Rounded: 1 Computed: 0.00  Non Adjusted: 0.00

  Access Path: TableScan

    Cost: 2.00  Resp: 2.00  Degree: 0

      Cost_io: 2.00  Cost_cpu: 7121

      Resp_io: 2.00  Resp_cpu: 7121

  Access Path: index (index (FFS))

    Index: AIRDATEINDEX

    resc_io: 32982.00  resc_cpu: 7554023805

    ix_sel: 0.000000  ix_sel_with_filters: 1.000000

  Access Path: index (FFS)

    Cost: 33186.78  Resp: 33186.78  Degree: 1

      Cost_io: 32982.00  Cost_cpu: 7554023805

      Resp_io: 32982.00  Resp_cpu: 7554023805

  Access Path: index (index (FFS))

    Index: DATEINDEX

    resc_io: 27762.00  resc_cpu: 7259606781

    ix_sel: 0.000000  ix_sel_with_filters: 1.000000

  Access Path: index (FFS)

    Cost: 27958.80  Resp: 27958.80  Degree: 1

      Cost_io: 27762.00  Cost_cpu: 7259606781

      Resp_io: 27762.00  Resp_cpu: 7259606781

Access Path: index(index (FFS))

    Index: DESDATEINDEX

    resc_io: 35534.00  resc_cpu: 7854852632

    ix_sel: 0.000000  ix_sel_with_filters: 1.000000

  Access Path: index (FFS)

    Cost: 35746.93  Resp: 35746.93  Degree: 1

      Cost_io: 35534.00  Cost_cpu: 7854852632

      Resp_io: 35534.00  Resp_cpu: 7854852632

  Access Path: index (index (FFS))

    Index: ORGDATEINDEX

    resc_io: 35255.00  resc_cpu: 7775449582

    ix_sel: 0.000000  ix_sel_with_filters: 1.000000

  Access Path: index (FFS)

    Cost: 35465.78  Resp: 35465.78  Degree: 1

      Cost_io: 35255.00  Cost_cpu: 7775449582

      Resp_io: 35255.00  Resp_cpu: 7775449582

 

Access Path: index(skip-scan)

    SS scan sel: 0.010000  SS filter sel: 0.010000  ANDV (#skips): 253.290000

    SS io: 1266.450000 vs. table scan io:2.000000

    Skip Scan rejected

  Access Path: index (FullScan)

    Index: AIRDATEINDEX

    resc_io: 121777.00  resc_cpu: 8734071799

    ix_sel: 1.000000  ix_sel_with_filters: 1.000000

 ***** Logdef predicate Adjustment ******

 Final IO cst 0.00 , CPU cst 50.00

 ***** End Logdef Adjustment ******

    Cost: 122013.77  Resp: 122013.77  Degree: 1

  Access Path: index (AllEqRange)

    Index: DATEINDEX

    resc_io: 1028.00  resc_cpu: 84140440

    ix_sel: 0.010000  ix_sel_with_filters: 0.010000

    Cost: 1030.28  Resp: 1030.28 Degree: 1

 

  Access Path: index (skip-scan)

    SS scan sel: 0.010000  SS filter sel: 0.010000  ANDV (#skips): 12165.640000

    SS io: 12165.640000 vs. table scan io:2.000000

kip Scan rejected

  Access Path: index (FullScan)

    Index: DESDATEINDEX

    resc_io: 131199.00  resc_cpu: 9076147207

    ix_sel: 1.000000  ix_sel_with_filters: 1.000000

 ***** Logdef predicate Adjustment ******

 Final IO cst 0.00 , CPU cst 50.00

 ***** End Logdef Adjustment ******

    Cost: 131445.04  Resp: 131445.04  Degree: 1

 

  Access Path: index (skip-scan)

    SS scan sel: 0.010000  SS filter sel: 0.010000  ANDV (#skips): 10271.460000

    SS io: 10271.460000 vs. table scan io:2.000000

    Skip Scan rejected

  Access Path: index (FullScan)

    Index: ORGDATEINDEX

    resc_io: 130171.00  resc_cpu: 8984023766

    ix_sel: 1.000000  ix_sel_with_filters: 1.000000

 ***** Logdef predicate Adjustment ******

 Final IO cst 0.00 , CPU cst 50.00

 ***** End Logdef Adjustment ******

    Cost: 130414.54  Resp: 130414.54  Degree: 1

  ****** trying bitmap/domain indexes ******

  ****** finished trying bitmap/domain indexes******

******** Begin indexjoin costing ********

****** tryingbitmap/domain indexes ******

  Access Path: index (AllEqRange)

    Index: DATEINDEX

    resc_io: 1028.00  resc_cpu: 84140440

    ix_sel: 0.010000  ix_sel_with_filters: 0.010000

    Cost: 1030.28  Resp: 1030.28 Degree: 0

  Bitmap nodes:

    Used DATEINDEX

      Cost = 1287.851144, sel = 0.010000

  ****** finished trying bitmap/domain indexes******

******** End index joincosting ********

  Best:: AccessPath: TableScan

         Cost: 2.00  Degree: 1 Resp: 2.00  Card: 0.00  Bytes: 0

 

Final cost for queryblock SEL$1 (#0) - All Rows Plan:

  Best join order: 1

  Cost: 2.0002 Degree: 1  Card: 1.0000  Bytes: 9

  Resc: 2.0002 Resc_io: 2.0000  Resc_cpu: 7121

  Resp: 2.0002 Resp_io: 2.0000  Resc_cpu: 7121

 

Oracle CBO計算了這條SQL各種執行計劃,認為全表掃描效率最高,成本cost值是2,因此執行計劃,如下所示,選擇全表掃描,

 

按照上面的計算,選擇TABLE ACCESS FULL無可厚非,可為何實際執行,時間非常的久?

 

看下錶的記錄數,有千萬級的資料!

SQL> select count(*) from user.tbl;

  COUNT(*)

--------------------

  39751845

 

檢視相鄰測試日期的資料,每日1萬左右記錄,

SQL> SELECT cki_date,COUNT(*) FROM tbl GROUP BY cki_date ORDER BY cki_date DESC;

1        2017/10/18        14378

2        2017/10/17        14213

3        2017/10/16        14127

4        2017/10/15        13841

5        2017/10/14        14436

6        2017/10/13        14745

7        2017/10/12        14389

8        2017/10/11        14616

9        2017/10/10        14671

10      2017/10/9          14953

 

其中有詐。 

 

檢索錶的歷史統計資訊,

SQL> alter session set nls_timestamp_tz_format='YYYY-MM-DD HH24:MI:SS';

Session altered.

 

SQL> select a.savtime, a.rowcnt, a.blkcnt, a.avgrln, a.analyzetime  

from sys.wri$_optstat_tab_history a, dba_objects b
where a.obj#=b.object_id and b.object_name='TBL';

SAVTIME                        ROWCNT      BLKCNT    AVGRLN ANALYZETIME
---------------------------------------- ---------- ---------- ------------------
2018-01-10 19:01:37        39209893      516232             87           01-JAN-18
2018-01-10 22:05:50                0                   1                  0           10-JAN-18

 

測試時間是1月11日,前一天22:00統計資訊採集任務,就認為表中記錄數為0,即是一張空表,但是由於索引統計資訊正確,顯示幾千萬資料,自然CBO認為全表掃描效率,要比索引掃描的效率低。

 

瞭解了下,由於做效能測試,10日曾經清理了表資料,11日進行了批量資料匯入,但是統計資訊,未進行手工採集,因此用了前一天自動採集的,導致統計資訊,和表的實際資訊不符,為CBO提供了錯誤的資訊,因而選錯了執行計劃。

 

dbsnake的書中已經指出,每日22:00定時採集的統計資訊,使用如下指令碼進行建立,

$ORACLE_HOME/rdbms/admin/catmwin.sql

 

如下是11g指令碼中,自動統計資訊採集的內容,

-- Create gather stats program.
BEGIN
dbms_scheduler.create_program(
  program_name=>'gather_stats_prog',
  program_type=>'STORED_PROCEDURE',
  program_action=>'dbms_stats.gather_database_stats_job_proc',
  number_of_arguments=>0,
  enabled=>TRUE,
  comments
      =>'Oracle defined automatic optimizer statistics collection program');
EXCEPTION
  when others then
    if sqlcode = -27477 then NULL;
    else raise;
    end if;
END;
/

 

一個名稱為gather_stats_prog的program,呼叫了儲存過程dbms_stats.gather_database_stats_job_proc,使用fyunwrap工具,破解儲存過程的原始碼,比較晦澀,大致看出些資訊,要是理解有錯誤,歡迎糾正,

...

// 執行以下SQL,選擇符合條件,需要使用自動統計資訊,進行採集的物件,

SELECT /*+ leading(ST o u) */
        ST.OBJ# OBJNUM,
        U.NAME OWNER, O.NAME OBJNAME, O.SUBNAME SUBNAME,
        ST.TYPE# TYPE#, ST.BO# BO#, ST.FLAGS, ST.OSIZE
      FROM
        OBJ$ O, USER$ U,
        (SELECT /*+ no_merge */
           *
         FROM
           (SELECT /*+ dynamic_sampling(s 4) dynamic_sampling_est_cdn(s) */
              S.*,
              ROW_NUMBER() OVER
                (ORDER BY         
                   DECODE(TYPE#,
                          1,2,2,1,
                          19,3,20,4,
                          34,5,35,6,7),
                  STALENESS,OSIZE,OBJ#) RN
            FROM
              STATS_TARGET$ S
            WHERE
              S.STATUS = DSC_AUTO_STATS_PENDING)

// 此處DSC_AUTO_STATS_PENDING預設值為0

         WHERE
           RN <= DSC_TARGET_LIST_BATCH_SIZE) ST

// 此處DSC_TARGET_LIST_BATCH_SIZE預設值為9000

      WHERE
        ST.OBJ# = O.OBJ# AND O.OWNER# = U.USER#
      ORDER BY       
        DECODE(ST.TYPE#,
               1,2,2,1,
               19,3,20,4,
               34,5,35,6,7),
        ST.STALENESS, ST.OSIZE, ST.OBJ#;

 

// 結果集儲存於,一個名稱為TARGETTAB的自定義型別,

TYPE TARGETREC IS RECORD (
    OBJNUM     NUMBER,
    OWNER      VARCHAR2(30),
    OBJNAME    VARCHAR2(30),
    SUBNAME    VARCHAR2(30),
    TYPE#      PLS_INTEGER,
    BO#        NUMBER,
    FLAGS      PLS_INTEGER,
    OSIZE      NUMBER);
  TYPE TARGETTAB IS TABLE OF TARGETREC;

 

// 遍歷檢索結果,

FOR I IN 1..TARGET.COUNT LOOP

...

 

// 根據TYPE#,判斷此物件型別,是表、索引、分割槽、子分割槽這些型別。對於索引,用瞭如下函式,如果TYPE#是1、20、35則判斷為索引,

FUNCTION IS_OBJTYPE_INDEX(OBJTYPE PLS_INTEGER) RETURN BOOLEAN IS
  BEGIN
    RETURN OBJTYPE IN (1, 20, 35);
  END IS_OBJTYPE_INDEX;

 

// 對於索引,會使用如下儲存過程,執行統計資訊的採集,

GATHER_INDEX_STATS_AUX('"' || TARGET(I).OWNER || '"',
                  DBMS_STATS_INTERNAL.DQ(TARGET(I).OBJNAME),
                  SUBNAME,
                  DEFAULT_ESTIMATE_PERCENT,
                  NULL, NULL, NULL, 
                  TO_DEGREE_TYPE(GET_PARAM('DEGREE')),
                  TMPGRAN,
                  DEFAULT_GRANULARITY,
                  TO_NO_INVALIDATE_TYPE(GET_PARAM('NO_INVALIDATE')),
                  'DATA',
                  START_TIME,
                  TRUE, FALSE, FALSE);

 

// 對於表則會使用,如下儲存過程,進行統計資訊的自動採集,注意其中是否級聯採集索引,使用的是DEFAULT_CASCADE,預設應為false(暫時未找著出處),

GATHER_TABLE_STATS_AUX(DBMS_STATS_INTERNAL.DQ(TARGET(I).OWNER),
                  DBMS_STATS_INTERNAL.DQ(TARGET(I).OBJNAME),
                  SUBNAME,
                  DEFAULT_ESTIMATE_PERCENT,
                  FALSE,
                  DEFAULT_METHOD_OPT,
                  TO_DEGREE_TYPE(GET_PARAM('DEGREE')),
                  TMPGRAN,
                  DEFAULT_GRANULARITY,
                  DEFAULT_CASCADE,
                  NULL, NULL, NULL,
                  TO_NO_INVALIDATE_TYPE(GET_PARAM('NO_INVALIDATE')),
                  FALSE, DSC_NON_FXT, 'DATA',
                  START_TIME,
                  TRUE, CMS_ONLY,FALSE, DSC_SEQ, NULL, JOBSTAB, NULL, FALSE); 

 

既然表的統計資訊不準,手工採集表和索引統計資訊,

SQL> exec dbms_stats.gather_table_stats('USER','TBL',cascade=>true);

PL/SQL proceduresuccessfully completed.

 

此時統計資訊,已經是準確的了,

SQL> select a.savtime, a.rowcnt, a.blkcnt, a.avgrln, a.analyzetime  

from sys.wri$_optstat_tab_history a, dba_objects b
where a.obj#=b.object_id and b.object_name='TBL';

SAVTIME                        ROWCNT      BLKCNT    AVGRLN ANALYZETIME
---------------------------------------- ---------- ---------- ------------------
2018-01-10 19:01:37        39209893      516232             87           01-JAN-18
2018-01-10 22:05:50                0                   1                  0           10-JAN-18

2018-01-11 10:10:35        39722903      524416             87           11-JAN-18

 

再對如下語句,執行10053,

SQL> explain planfor select count(*) from user.tbl where cki_date =to_date('2017-10-10','yyyy-mm-dd');

Explained.

 

檢索相應的trace檔案,

***************************************

BASE STATISTICALINFORMATION

***********************

Table Stats::

  Table: TBL_CSTD  Alias: TBL_CSTD

    #Rows: 39751845  #Blks: 524416  AvgRowLen:  87.00 ChainCnt:  0.00

Index Stats::

  Index: AIRDATEINDEX  Col#: 1 3

    LVLS: 3 #LB: 123176  #DK: 287186  LB/K: 1.00 DB/K: 4.00  CLUF: 1323872.00

  Index: DATEINDEX  Col#: 3

    LVLS: 2 #LB: 111409  #DK: 5040  LB/K: 22.00 DB/K: 154.00  CLUF: 779959.00

  Index: DESDATEINDEX  Col#: 5 3

    LVLS: 3 #LB: 125310  #DK: 1259074  LB/K: 1.00 DB/K: 10.00  CLUF: 13360966.00

  Index: ORGDATEINDEX  Col#: 4 3

    LVLS: 3 #LB: 131116  #DK: 990166  LB/K: 1.00 DB/K: 13.00  CLUF: 13438679.00

Access path analysisfor TBL_CSTD

***************************************

SINGLE TABLE ACCESSPATH

  Single Table Cardinality Estimation forTBL_CSTD[TBL_CSTD]

  Column (#3): CKI_DATE(

    AvgLen: 8 NDV: 5040 Nulls: 0 Density:0.000198 Min: 2453006 Max: 2458045

  Table: TBL_CSTD  Alias: TBL_CSTD

    Card: Original: 39751845.000000  Rounded: 7887 Computed: 7887.27  Non Adjusted:7887.27

  Access Path: TableScan

    Cost: 142390.87  Resp: 142390.87  Degree: 0

      Cost_io: 142031.00  Cost_cpu: 13275039879

      Resp_io: 142031.00  Resp_cpu: 13275039879

  Access Path: index (index (FFS))

    Index: AIRDATEINDEX

    resc_io: 33362.00  resc_cpu: 7656349173

    ix_sel: 0.000000  ix_sel_with_filters: 1.000000

  Access Path: index (FFS)

    Cost: 33569.55  Resp: 33569.55  Degree: 1

      Cost_io: 33362.00  Cost_cpu: 7656349173

      Resp_io: 33362.00  Resp_cpu: 7656349173

  Access Path: index (index (FFS))

    Index: DATEINDEX

    resc_io: 30175.00  resc_cpu: 7883992719

    ix_sel: 0.000000  ix_sel_with_filters: 1.000000

  Access Path: index (FFS)

    Cost: 30388.72  Resp: 30388.72  Degree: 1

      Cost_io: 30175.00  Cost_cpu: 7883992719

      Resp_io: 30175.00  Resp_cpu: 7883992719

  Access Path: index (index (FFS))

    Index: DESDATEINDEX

    resc_io: 33940.00  resc_cpu: 7492760386

    ix_sel: 0.000000  ix_sel_with_filters: 1.000000

  Access Path: index (FFS)

    Cost: 34143.12  Resp: 34143.12  Degree: 1

      Cost_io: 33940.00  Cost_cpu: 7492760386

      Resp_io: 33940.00  Resp_cpu: 7492760386

  Access Path: index (index (FFS))

    Index: ORGDATEINDEX

    resc_io: 35512.00  resc_cpu: 7852113037

    ix_sel: 0.000000  ix_sel_with_filters: 1.000000

  Access Path: index (FFS)

    Cost: 35724.86  Resp: 35724.86  Degree: 1

      Cost_io: 35512.00  Cost_cpu: 7852113037

      Resp_io: 35512.00  Resp_cpu: 7852113037

Access Path: index(skip-scan)

    SS scan sel: 0.000198  SS filter sel: 0.000198  ANDV (#skips): 193.000000

    SS io: 193.000000 vs. table scan io:142031.000000

    Skip Scan chosen

  Access Path: index (SkipScan)

    Index: AIRDATEINDEX

    resc_io: 196.00  resc_cpu: 2978402

    ix_sel: 0.000198  ix_sel_with_filters: 0.000198

    Cost: 196.08  Resp: 196.08 Degree: 1

  Access Path: index (AllEqRange)

    Index: DATEINDEX

    resc_io: 25.00  resc_cpu: 1833236

    ix_sel: 0.000198  ix_sel_with_filters: 0.000198

    Cost: 25.05 Resp: 25.05  Degree: 1

 

  Access Path: index (skip-scan)

    SS scan sel: 0.000198  SS filter sel: 0.000198  ANDV (#skips): 582.000000

    SS io: 582.000000 vs. table scan io:142031.000000

    Skip Scan chosen

  Access Path: index (SkipScan)

    Index: DESDATEINDEX

    resc_io: 585.00  resc_cpu: 5706842

    ix_sel: 0.000198  ix_sel_with_filters: 0.000198

    Cost: 585.15  Resp: 585.15 Degree: 1

Access Path: index(skip-scan)

    SS scan sel: 0.000198  SS filter sel: 0.000198  ANDV (#skips): 421.000000

    SS io: 421.000000 vs. table scan io:142031.000000

    Skip Scan chosen

  Access Path: index (SkipScan)

    Index: ORGDATEINDEX

    resc_io: 424.00  resc_cpu: 4634491

    ix_sel: 0.000198  ix_sel_with_filters: 0.000198

    Cost: 424.13  Resp: 424.13 Degree: 1

  ****** trying bitmap/domain indexes ******

  ****** finished trying bitmap/domain indexes******

  Best:: AccessPath: IndexRange

  Index: DATEINDEX

         Cost: 25.05  Degree: 1 Resp: 25.05  Card: 7887.27  Bytes: 0

 

Final cost for queryblock SEL$1 (#0) - All Rows Plan:

  Best join order: 1

  Cost: 25.0497 Degree: 1  Card: 7887.0000  Bytes: 63096

  Resc: 25.0497 Resc_io: 25.0000  Resc_cpu:1833236

  Resp: 25.0497 Resp_io: 25.0000  Resc_cpu:1833236

 

此時選擇的最佳執行計劃,是cki_date欄位索引掃描, 

 

總結:

1. 正如dbsnake書中所說,若系統批量匯入資料,建議業務使用前,立即採集相關表的統計資訊,因為每日22:00,才會進行統計資訊自動採集,之間的時間差,就有可能因為統計資訊不準,讓CBO選錯執行計劃。

2. 雖然CBO對於執行計劃cost計算,屬於機密,但是10053可以間接,讓我們瞭解CBO如何選擇,某一個執行計劃,再根據表、索引等統計資訊,結合來看,有可能就發現一些線索。