問題現象
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.8213654348≈59118 與上述執行計中CBO計算的一致
OK,到此問題就明朗了,Oracle在CBO計算varchar比較關系的時候會將varchar數據通過計算得出raw值,通過raw來進行比較計算,並依此來計算選擇率。