Oracle資料遷移後由列的直方圖統計資訊引起的執行計劃異常

gegeman發表於2020-06-08

(一)問題背景

在使用impdp進行資料匯入的時候,往往在匯入表和索引的統計資訊的時候,速度非常慢,因此我在使用impdp進行匯入時,會使用exclude=table_statistics排除表的統計資訊,從而加快匯入速度,之後再手動收集統計資訊。

clipboard

                                              圖.impdp匯入資料的時匯入統計資訊速度非常慢

匯入語句如下:

impdp user/password directory=DUMPDIR dumpfile=TEST01.dmp logfile=TEST01.log remap_schema=TEST_USER:TEST_USER123 exclude=table_statistics

手動收集統計資訊語句如下:

EXEC dbms_stats.gather_table_stats(ownname => 'LIJIAMAN',tabname => 'TEST01');

最近使用以上方法將資料還原到測試環境後,發現與生產環境執行計劃存在偏差,本來應該走全表掃描的,卻走了索引範圍掃描。經過確認,是由於列的直方圖統計資訊未收集引發的執行計劃偏差。


(二)列的直方圖統計資訊

什麼是列的直方圖統計資訊呢?在Oracle資料庫中,Oracle預設列上的值是在最小值與最大值之間均分佈的,當在計算cardinatity時,會以均勻分佈的方式計算,但是在實際生活中某些場景下資料並非均勻分佈。舉個列子,某公司有員工10000人,表A的列COL1記錄員工的績效(分別是:A、B、C、D,A最好,D最差),那麼可能A佔了15%,B佔了60,C佔了20%,D佔了5%。很明顯在該場景下資料並非均勻分佈,假如以均勻分佈的方式去統計員工的績效,可能會導致執行計劃失準。

當列的資料分佈不均勻的時候,就需要統計列上的資料分佈情況,從而走出正確的執行計劃,列的直方圖統計資訊就是記錄列上的資料分佈情況的。


(三)異常模擬

STEP1:建立測試表test01

create table test01
(id number,
name varchar2(10)
);
create index idx_test01_id on test01(id);

向test01中插入測試資料

begin
insert into test01 values(1,'a');

for i in 1..10 loop
insert into test01 values(2,'b');
end loop;

for i in 1..100 loop
insert into test01 values(3,'c');
end loop;

for i in 1..1000 loop
insert into test01 values(4,'d');
end loop;

commit;
end;

檢視資料分佈情況:

SQL> SELECT ID,NAME,COUNT(*) FROM test01 GROUP BY ID,NAME ORDER BY COUNT(*);

ID          NAME       COUNT(*)
---------- ---------- ----------
1           a          1       
2           b          10
3           c          100
4           d          1000


STEP2:收集統計資訊,因為上面查詢過id列,故在收集統計資訊的時候,會收集直方圖的統計資訊

EXEC dbms_stats.gather_table_stats(ownname => 'LIJIAMAN',tabname => 'TEST01');

檢視是否已經收集了直方圖資訊,發現id列上已經收集

SQL> SELECT a.OWNER,a.TABLE_NAME,a.COLUMN_NAME,a.LOW_VALUE,a.HIGH_VALUE,a.NUM_BUCKETS,a.HISTOGRAM
2 FROM dba_tab_columns a
3 WHERE a.OWNER = 'LIJIAMAN' AND a.TABLE_NAME = 'TEST01';

OWNER     TABLE_NAME   COLUMN_NAME   LOW_VALUE     HIGH_VALUE    NUM_BUCKETS   HISTOGRAM
--------- -----------  ------------  ------------  ------------  -----------  ---------------
LIJIAMAN  TEST01       ID            C102          C105          4             FREQUENCY
LIJIAMAN  TEST01       NAME          61            64            1             NONE

檢視直方圖,已經將id列的4個值放入了4個bucket中:

SQL> SELECT * FROM dba_tab_histograms a WHERE a.OWNER = 'LIJIAMAN' AND a.TABLE_NAME = 'TEST01';

OWNER        TABLE_NAME    COLUMN_NAME    ENDPOINT_NUMBER    ENDPOINT_VALUE ENDPOINT_ACTUAL_VALUE
-----------  ------------  -------------  ---------------    -------------- ----------------------
LIJIAMAN     TEST01        ID                           1                 1 
LIJIAMAN     TEST01        ID                          11                 2 
LIJIAMAN     TEST01        ID                         111                 3 
LIJIAMAN     TEST01        ID                        1111                 4 
LIJIAMAN     TEST01        NAME                         0    5.036527952778 
LIJIAMAN     TEST01        NAME                         1    5.192296858534


STEP3:檢視id=1和id=4的執行計劃,當id=1時,走索引範圍掃描,當id=4時,走全表掃描

id列存在直方圖統計資訊,當id=1時,走索引範圍掃描 id列存在直方圖統計資訊,當id=4時,走全表掃描
SELECT * FROM test01 WHERE ID=1

 Plan Hash Value  : 1151852672 

----------------------------------------------------------------------------------------
| Id  | Operation                     | Name          | Rows | Bytes | Cost | Time     |
----------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT              |               |    1 |     5 |    2 | 00:00:01 |
|   1 |   TABLE ACCESS BY INDEX ROWID | TEST01        |    1 |     5 |    2 | 00:00:01 |
| * 2 |    INDEX RANGE SCAN           | IDX_TEST01_ID |    1 |       |    1 | 00:00:01 |
----------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
------------------------------------------
* 2 - access("ID"=1)
SELECT * FROM test01 WHERE ID=4

 Plan Hash Value  : 262542483 

-----------------------------------------------------------------------
 | Id  | Operation           | Name   | Rows | Bytes | Cost | Time     |
-----------------------------------------------------------------------
 |   0 | SELECT STATEMENT    |        | 1000 |  5000 |    3 | 00:00:01 |
 | * 1 |   TABLE ACCESS FULL | TEST01 | 1000 |  5000 |    3 | 00:00:01 |
-----------------------------------------------------------------------

Predicate Information (identified by operation id):
 ------------------------------------------
 * 1 - filter("ID"=4)


STEP4:接下來模擬資料遷移,排除統計資訊

匯出表test01

expdp lijiaman/lijiaman directory=DUMPDIR tables=LIJIAMAN.TEST01 dumpfile =test01.dmp

刪除原來的表:

SQL> drop table test01;
Table dropped

再次匯入表,排除統計資訊:

impdp lijiaman/lijiaman directory=DUMPDIR dumpfile =test01.dmp exclude=table_statistics

檢視錶的統計資訊,不存在統計資訊:

SQL> SELECT   a.OWNER,a.TABLE_NAME,a.COLUMN_NAME,a.LOW_VALUE,a.HIGH_VALUE,a.NUM_BUCKETS,a.HISTOGRAM
   2  FROM     dba_tab_columns a
   3  WHERE    a.OWNER = 'LIJIAMAN' AND a.TABLE_NAME = 'TEST01';

OWNER          TABLE_NAME      COLUMN_NAME     LOW_VALUE    HIGH_VALUE   NUM_BUCKETS HISTOGRAM
 -------------- --------------- --------------- ------------ ------------ ----------- ---------------
 LIJIAMAN       TEST01          ID                                                    NONE
 LIJIAMAN       TEST01          NAME                                                  NONE


STEP5:手動收集統計資訊

EXEC dbms_stats.gather_table_stats(ownname => 'LIJIAMAN',tabname => 'TEST01');

發現統計資訊已經收集,但是不存在直方圖的統計資訊

SQL> SELECT   a.OWNER,a.TABLE_NAME,a.COLUMN_NAME,a.LOW_VALUE,a.HIGH_VALUE,a.NUM_BUCKETS,a.HISTOGRAM
  2  FROM     dba_tab_columns a
  3  WHERE    a.OWNER = 'LIJIAMAN' AND a.TABLE_NAME = 'TEST01';

OWNER     TABLE_NAME  COLUMN_NAME  LOW_VALUE   HIGH_VALUE  NUM_BUCKETS HISTOGRAM
--------- ----------- -----------  ----------- ----------- ----------- ---------------
LIJIAMAN  TEST01      ID           C102        C105                  1 NONE
LIJIAMAN  TEST01      NAME         61          64                    1 NONE


STEP6:再次檢視id=1和id=4的執行計劃,當id=1或id=4時,都走索引範圍掃描

id列未收集直方圖統計資訊,當id=1時,走索引範圍掃描 id列未收集直方圖統計資訊,當id=4時,走索引範圍掃描
SELECT * FROM test01 WHERE ID=1
 Plan Hash Value  : 1151852672 

----------------------------------------------------------------------------------------
| Id  | Operation                     | Name          | Rows | Bytes | Cost | Time     |
----------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT              |               |  278 |  1390 |    2 | 00:00:01 |
|   1 |   TABLE ACCESS BY INDEX ROWID | TEST01        |  278 |  1390 |    2 | 00:00:01 |
| * 2 |    INDEX RANGE SCAN           | IDX_TEST01_ID |  278 |       |    1 | 00:00:01 |
----------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
------------------------------------------
* 2 - access("ID"=1)
SELECT * FROM test01 WHERE ID=4

 Plan Hash Value  : 1151852672 

----------------------------------------------------------------------------------------
| Id  | Operation                     | Name          | Rows | Bytes | Cost | Time     |
----------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT              |               |  278 |  1390 |    2 | 00:00:01 |
|   1 |   TABLE ACCESS BY INDEX ROWID | TEST01        |  278 |  1390 |    2 | 00:00:01 |
| * 2 |    INDEX RANGE SCAN           | IDX_TEST01_ID |  278 |       |    1 | 00:00:01 |
----------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
------------------------------------------
* 2 - access("ID"=4)


STEP7:再次收集統計資訊,因為使用過了id列作為查詢條件,故再次收集統計資訊時,會收集id列的直方圖資訊:

EXEC dbms_stats.gather_table_stats(ownname => 'LIJIAMAN',tabname => 'TEST01');

可以看到,此時已經收集了id列的直方圖統計資訊:

SQL> SELECT   a.OWNER,a.TABLE_NAME,a.COLUMN_NAME,a.LOW_VALUE,a.HIGH_VALUE,a.NUM_BUCKETS,a.HISTOGRAM
  2  FROM     dba_tab_columns a
  3  WHERE    a.OWNER = 'LIJIAMAN' AND a.TABLE_NAME = 'TEST01';

OWNER                          TABLE_NAME                     COLUMN_NAME                    LOW_VALUE     HIGH_VALUE    NUM_BUCKETS HISTOGRAM
------------------------------ ------------------------------ ------------------------------ ------------- ------------- ----------- ---------------
LIJIAMAN                       TEST01                         ID                             C102          C105                    4 FREQUENCY
LIJIAMAN                       TEST01                         NAME                           61            64                      1 NONE

執行計劃已經按照我們想要的方式走:

id列重新收集直方圖統計資訊,當id=1時,走索引範圍掃描 id列重新收集直方圖統計資訊,當id=4時,走全表掃描
SELECT * FROM test01 WHERE ID=1

 Plan Hash Value  : 1151852672 

----------------------------------------------------------------------------------------
| Id  | Operation                     | Name          | Rows | Bytes | Cost | Time     |
----------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT              |               |    1 |     5 |    2 | 00:00:01 |
|   1 |   TABLE ACCESS BY INDEX ROWID | TEST01        |    1 |     5 |    2 | 00:00:01 |
| * 2 |    INDEX RANGE SCAN           | IDX_TEST01_ID |    1 |       |    1 | 00:00:01 |
----------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
------------------------------------------
* 2 - access("ID"=1)
SELECT * FROM test01 WHERE ID=4

 Plan Hash Value  : 262542483 

-----------------------------------------------------------------------
| Id  | Operation           | Name   | Rows | Bytes | Cost | Time     |
-----------------------------------------------------------------------
|   0 | SELECT STATEMENT    |        | 1000 |  5000 |    3 | 00:00:01 |
| * 1 |   TABLE ACCESS FULL | TEST01 | 1000 |  5000 |    3 | 00:00:01 |
-----------------------------------------------------------------------

Predicate Information (identified by operation id):
------------------------------------------
* 1 - filter("ID"=4)


(四)總結

在使用expdp/impdp進行匯出/匯入資料的時,統計資訊是非常重要的,對於大部分統計資訊,我們可以在匯入結束之後收集獲得。但是對於列的直方圖統計資訊,Oracle預設收集的方式是auto,即Oracle會根據使用者對列的使用情況進行判斷是否收集直方圖統計資訊,然而資料剛遷移完成,在表還未使用的情況下收集統計資訊,往往收集不到列的直方圖資訊,這就造成了執行計劃異常,這種情況通常在下一次收集統計資訊之後會有所改變。



參考文件:

DBMS_STATS With METHOD_OPT =>'..SIZE auto' May Not Collect Histograms (Doc ID 557594.1)

相關文章