實際案例告訴你為什么Oracle不建議使用varchar2來存時間數據


問題現象
2015年9月客戶系統中一條高邏輯讀的SQL語句,在業務高峰期執行頻率較高,導致系統邏輯讀居高不下,同時帶高了系統CPU,SQL語句主體部分如下

SELECT /* ^^*/
COUNT(DISTINCT ts_map.draftid) AS recordCount
FROM usr.BillStateMap ts_map
INNER JOIN usr.create ts_create
ON ts_create.draftid = ts_map.draftid
LEFT JOIN usr.search ts_search
ON ts_create.draftid = ts_search.draftid
LEFT JOIN usr.accept accept
ON ts_create.draftid = accept.draftid
LEFT JOIN usr.kfvistirecord ts_kfback
ON ts_kfback.draftid = ts_create.draftid
LEFT JOIN (SELECT DISTINCT .. .. .. . FROM usr.create_user t_user) ts_user
ON ts_create.draftid = ts_user.creatinfoid
WHERE 1 = 1
AND (ts_create.location = '0579' OR ts_user.location = '0579')
AND ts_create.CREATETIME >= '2015-06-23 00:00:00'
AND ts_create.CREATETIME <= '2015-09-21 23:59:59'
AND ts_map.billstate = '待報結'
ORDER BY ts_create.draftId DESC;

通過SQL語句的過濾謂詞來確定SQL的過濾情況

通過執行計划可以看出SQL語句走的驅動表是usr.create,但通過過濾謂詞檢查的結果可以看出實際驅動表應該是usr.BillStateMap
進一步檢查SQL語句相關表的索引(檢查統計信息是最新收集的,排除統計信息不准的問題)

原因分析:
在此我們回顧下Oracle執行計划走NESTED LOOPS時的一些條件
1、過濾后的小表作為驅動表,大表作為被驅動表,表的大小由CBO評估后計算得到的。
2、被驅動表關聯列需要有索引,這條SQL的關聯列是DRAFTID,
通過上述索引的查詢可以排除第二個條件,兩個表上關聯列上均有索引,所以當前SQL語句走錯驅動表應該是CBO計算過濾后返回行出現錯誤導致的
檢查BILLSTATEMAP表的BILLSTATE是否存在直方圖

SQL> SELECT OWNER, TABLE_NAME, COLUMN_NAME, HISTOGRAM
  2    FROM DBA_TAB_COLUMNS
  3   WHERE TABLE_NAME = 'BILLSTATEMAP'
  4     AND HISTOGRAM <> 'NONE';
OWNER                TABLE_NAME                     COLUMN_NAME                    HISTOGRAM
-------------------- ------------------------------ ------------------------------ ---------------
NETFORCE             BILLSTATEMAP                   BILLSTATE                      FREQUENCY

可以看出雖然BILLSTATE字段上存在數據選擇性不佳,切存在數據傾斜,但是該字段上存在直方圖,並不會導致CBO在計算謂詞過濾的時候出現錯誤,所以最終焦點確定在了create表上,為什么實際上create表過濾后有358760行,CBO任然選擇他作為驅動表?
作為CBO的正常行為,通過計算后確定過濾后的行數少,即可作為驅動表, 基於成本計算來確定驅動表和被驅動表。 嘗試使用hint來指定SQL語句的驅動表/*+ use_nl(ts_map,ts_create) leading(ts_map) */,得到執行計划如下

可以看出SQL的執行計划比較完美了,邏輯讀大幅下降,再次看下create表在內存中的執行計划,可以發現E-Rows和A-Rows相差好幾個數量級, E-Rows:代表着CBO評估的返回行數 A-Rows:代表SQL語句實際返回的行數

SQL> SELECT COUNT(*)
  2    FROM CREATE ts_create
  3   WHERE ts_create.CREATETIME >= '2015-06-23 00:00:00'
  4     AND ts_create.CREATETIME <= '2015-09-21 23:59:59';
 
  COUNT(*)
----------
    358760
 
Elapsed: 00:00:00.31
-------------------------------------------------------------------------------------------
| Id  | Operation          | Name       | Starts | E-Rows | A-Rows |   A-Time   | Buffers |
-------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT   |            |      1 |        |      1 |00:00:00.31 |   19571 |
|   1 |  SORT AGGREGATE    |            |      1 |      1 |      1 |00:00:00.31 |   19571 |
|*  2 |   TABLE ACCESS FULL| TAB_CREATE |      1 |      1 |    358K|00:00:00.25 |   19571 | 
-------------------------------------------------------------------------------------------

可以確定SQL語句走錯驅動表根本原因是CBO評估出錯誤的返回行,導致驅動表走錯,但是為什么會計算錯誤? 回顧下CBO在計算選擇率的公式如下,在沒有直方圖統計信息足夠新的情況下

"col BETWEEN val1 AND val2"的選擇率計算如下
Sel = ((val2 - val1) / (high_value - low_value) + (2 / NDV)) * A4Nulls
注:
high_value  代表過濾列col的最大值
low_value   代表過濾列col的最小值
NDV   代表數據列的非重復值數量,即distinct_key
A4Nulls   代表該字段的非空率

CBO的rows是通過選擇率計算出來的,所以選擇率的計算直接影響着rows的結果
在統計信息准確的情況下,NDV、A4Nulls、high_value、low_value都是定值並且認為是准確的,在此條件下影響選擇率的計算就只有val1和val2(即上述業務SQL在ts_create.CREATETIME上的過濾謂詞)
AND ts_create.CREATETIME >= '2015-06-23 00:00:00'
AND ts_create.CREATETIME <= '2015-09-21 23:59:59'

在此回頭查看SQL語句執行計划Predicate Information部分可以看出ts_create.CREATETIME字段上沒有進行to_date隱式轉換,也就是說ts_create.CREATETIME字段本身就是varchar2類型的,是否是由於varchar2類型的數據進行大小判斷導致的呢? 做了如下實驗: 創建相同數據的兩張表TAB_CREATE1和TAB_CREATE2,字段CREATETIME數據類型分別為varchar2和date,相關表查詢通過CREATETIME字段過濾后的執行計划如下

SQL> SELECT COUNT(*)
  2    FROM TAB_CREATE1 ts_create
  3   WHERE ts_create.CREATETIME >= '2015-06-23 00:00:00'
  4     AND ts_create.CREATETIME <= '2015-09-21 23:59:59';
 
  COUNT(*)
----------
    358760
-------varchar2類型的存放時間的情況下E-Rows和A-Rows相差好幾個數量級
Elapsed: 00:00:00.31
-------------------------------------------------------------------------------------------
| Id  | Operation          | Name       | Starts | E-Rows | A-Rows |   A-Time   | Buffers |
-------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT   |            |      1 |        |      1 |00:00:00.31 |   19571 |
|   1 |  SORT AGGREGATE    |            |      1 |      1 |      1 |00:00:00.31 |   19571 |
|*  2 |   TABLE ACCESS FULL| TAB_CREATE1|      1 |      1 |    358K|00:00:00.25 |   19571 | 
-------------------------------------------------------------------------------------------
---date類型的存放時間的情況下E-Rows和A-Rows 在一個數量級了
-------------------------------------------------------------------------------------------
| Id  | Operation          | Name       | Starts | E-Rows | A-Rows |   A-Time   | Buffers |
-------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT   |            |      1 |        |      1 |00:00:00.27 |   19571 |
|   1 |  SORT AGGREGATE    |            |      1 |      1 |      1 |00:00:00.27 |   19571 |
|*  2 |   TABLE ACCESS FULL| TAB_CREATE2|      1 |    156K|    358K|00:00:00.22 |   19571 |
-------------------------------------------------------------------------------------------

到這里問題就進一步明確了,是由於用varchar2類型存放數據導致的問題。 那么又有疑問了,Oracle在CBO是如何處理字符串大小比較的呢?其實沒有那么復雜,CBO會先將varchar2數據轉換統一轉換為raw數據后再做大小比較
繼續進行實驗,生成測試數據

-----創建個測試表
create table tab_yong as 
select rownum as id,
               to_char(to_date('2015-01-01 00:00:00','yyyy-mm-dd hh24:mi:ss') + (rownum*133)/24/3600, 'yyyy-mm-dd hh24:mi:ss') as c_date,
               (to_date('2015-01-01 00:00:00','yyyy-mm-dd hh24:mi:ss') + (rownum*133)/24/3600) as d_date,
               dbms_random.string('x', 20) random_string
          from dual
        connect by level <= 1500000;
-----收集表的統計信息
SQL> DECLARE
  2  BEGIN
  3    DBMS_STATS.GATHER_TABLE_STATS(ownname => 'YONG',
  4                                  tabname => 'TAB_YONG',
  5                                  estimate_percent => 100,
  6                                  method_opt =>'for all columns size repeat',                                                    
  7                                  no_invalidate => FALSE, cascade => TRUE,
  8                                  degree => 16);
  9  END;
 10  /
PL/SQL procedure successfully completed.
Elapsed: 00:00:15.07

測試表統計信息如下

使用varchar2 字段的SQL語句執行計划

SQL> select *
  2  from   TAB_YONG b
  3  where  b.c_date >= '2015-06-23 00:00:00'
  4  and    b.c_date <= '2015-09-21 23:59:59';
59116 rows selected.
Elapsed: 00:00:00.53
Execution Plan
----------------------------------------------------------
Plan hash value: 4254277647
-----------------------------------------------------------------------------------------
| Id  | Operation                   | Name      | Rows  | Bytes | Cost (%CPU)| Time     |
-----------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT            |           |     1 |    54 |     4   (0)| 00:00:01 |
|   1 |  TABLE ACCESS BY INDEX ROWID| TAB_YONG  |     1 |    54 |     4   (0)| 00:00:01 |
|*  2 |   INDEX RANGE SCAN          | IDX_CDATE |     1 |       |     3   (0)| 00:00:01 |
-----------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
   2 - access("B"."C_DATE">='2015-06-23 00:00:00' AND "B"."C_DATE"<='2015-09-21
              23:59:59')

使用date類型的SQL語句執行計划

SQL> select *
  2  from   TAB_YONG b
  3  where  b.d_date >= TO_DATE('2015-06-23 00:00:00')
  4  and    b.d_date <= TO_DATE('2015-09-21 23:59:59');
59116 rows selected.
Elapsed: 00:00:00.53
Execution Plan
----------------------------------------------------------
Plan hash value: 1934174936
-----------------------------------------------------------------------------------------
| Id  | Operation                   | Name      | Rows  | Bytes | Cost (%CPU)| Time     |
-----------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT            |           | 59118 |  3117K|   655   (1)| 00:00:08 |
|   1 |  TABLE ACCESS BY INDEX ROWID| TAB_YONG  | 59118 |  3117K|   655   (1)| 00:00:08 |
|*  2 |   INDEX RANGE SCAN          | IDX_DDATE | 59118 |       |   159   (0)| 00:00:02 |
-----------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
   2 - access("B"."D_DATE">=TO_DATE(' 2015-06-23 00:00:00', 'syyyy-mm-dd
              hh24:mi:ss') AND "B"."D_DATE"<=TO_DATE(' 2015-09-21 23:59:59', 'syyyy-mm-dd
              hh24:mi:ss'))

通過上述執行計划可以看出來,使用varchar2類型的CBO評估出來的rows是1行,而date評估出來的行數是59118行,與實際的59116行一致
轉換也沒有想象中的復雜,通過下面的函數就可以進行varchar2轉換到raw,具體函數代碼詳見本文結尾
通過上述函數我們計算下問題SQL的時間過濾謂詞值

SQL> col var_raw for a40
SQL> select get_internal_value('2015-06-23 00:00:00') var_raw from dual;
VAR_RAW
----------------------------------------
260592297225015000000000000000000000
SQL> col var_raw for a40
SQL> select get_internal_value('2015-09-21 23:59:59') var_raw from dual;
VAR_RAW
----------------------------------------
260592297225015000000000000000000000

計算結果可以看出兩個值是相等的,也就是說CBO通過計算后認為下列的條件是等價的

AND    ts_create.CREATETIME >= '2015-06-23 00:00:00'
AND    ts_create.CREATETIME <= '2015-09-21 23:59:59'
CBO層面等價於下列條件
AND ts_create.CREATETIME = '2015-09-21 23:59:59'

而等值條件的選擇率計算公式如下:
Sel = (1 / NDV) * A4Nulls

帶入統計信息分別計算下兩個選擇率的值及E-rows

----等值情況下
Sel = (1 / NDV) * A4Nulls  =(1/1500000) * (1500000-0)/1500000=1/1500000
rows=Sel * NumRows=1/1500000 * 1500000=1
注:
NumRows =dba_tables.num_rows 表示全表的行數

-----實際between情況下
Sel = ((val2 - val1) / (high_value - low_value) + (2 / NDV)) * A4Nulls
    =((to_date('2015-09-21 23:59:59', 'yyyy-mm-dd hh24:mi:ss') -
       to_date('2015-06-23 00:00:00', 'yyyy-mm-dd hh24:mi:ss')) /
       (to_date('2021-04-28 00:40:00', 'yyyy-mm-dd hh24:mi:ss') -
       to_date('2015-01-01 00:02:13', 'yyyy-mm-dd hh24:mi:ss')) +
       (2 / 1500000)) * 1
       =0.0394118809102899
rows=Sel * NumRows=0.0394118809102899 * 1500000=59117.821365434859118    與上述執行計中CBO計算的一致

OK,到此問題就明朗了,Oracle在CBO計算varchar比較關系的時候會將varchar數據通過計算得出raw值,通過raw來進行比較計算,並依此來計算選擇率。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM