環境:
spring jdbc 3.2.3.RELEASE
oracle 11.2.0.1.0 - Production
生產環境存在大表(3000萬數據):WR_MP_HOURW_R,包含復合主鍵:
mp_cp char(13),
DT date
對應索引名稱:WR_MP_HOURW_R_PRI。
問題:針對此表的更新很慢,每條update大概需要2秒。更新的時候使用的where 條件包含
mp_cp =? and DT =?
問題分析:
通過AWR 查找到針對此表的更新有大量的物理讀,因此判定更新時的執行計劃有問題。通過查找對應sql的執行計劃,發現沒有走索引。具體查找過程如下:
1)找到對應的sql_id:通過select t.SQL_TEXT,t.SQL_ID,t.ADDRESS,t.HASH_VALUE from v$sql t where t.SQL_TEXT like '%WR_MP_HOURW_R%';
2) 根據sql_id查找計劃:select * from table(dbms_xplan.display_cursor('sql_id',null,'advanced'));
果然發現sql沒有走索引,而是走的 table access full。正常應該走 INDEX UNIQUE SCAN。具體計劃如下:
SQL_ID 16gzsf0ccjjwg, child number 0
-------------------------------------
update sl_szy_mwr_intra .WR_MP_HOURW_R set MP_CD = :1 ,SPE_REG_DATA =
:2 ,HOUR_W = :3 ,DT = :4 where MP_CD = :5 and DT = :6
Plan hash value: 3498191616
-------------------------------------------------------------------------------------
| Id | Operation | Name | E-Rows |E-Bytes| Cost (%CPU)| E-Time |
-------------------------------------------------------------------------------------
| 0 | UPDATE STATEMENT | | | | 35061 (100)| |
| 1 | UPDATE | WR_MP_HOURW_R | | | | |
|* 2 | TABLE ACCESS FULL| WR_MP_HOURW_R | 21 | 504 | 35061 (1)| 00:07:01 |
-------------------------------------------------------------------------------------
Query Block Name / Object Alias (identified by operation id):
-------------------------------------------------------------
1 - UPD$1
2 - UPD$1 / WR_MP_HOURW_R@UPD$1
Outline Data
-------------

/**//*+
BEGIN_OUTLINE_DATA
IGNORE_OPTIM_EMBEDDED_HINTS
OPTIMIZER_FEATURES_ENABLE('11.2.0.3')
DB_VERSION('11.2.0.3')
ALL_ROWS
OUTLINE_LEAF(@"UPD$1")
FULL(@"UPD$1" "WR_MP_HOURW_R"@"UPD$1")
END_OUTLINE_DATA
*/
Peeked Binds (identified by position):
--------------------------------------
5 - (VARCHAR2(30), CSID=852): '2108811006101'
Predicate Information (identified by operation id):
---------------------------------------------------
2 - filter(("MP_CD"=:5 AND INTERNAL_FUNCTION("DT")=:6))
Column Projection Information (identified by operation id):
-----------------------------------------------------------
2 - (upd=2,3,4,5; cmp=2,3; cpy=2,3) "WR_MP_HOURW_R".ROWID[ROWID,10],
"MP_CD"[CHARACTER,13], "DT"[DATE,7], "HOUR_W"[NUMBER,22],
"SPE_REG_DATA"[CHARACTER,1]
Note
-----
- Warning: basic plan statistics not available. These are only collected when:
* hint 'gather_plan_statistics' is used for the statement or
* parameter 'statistics_level' is set to 'ALL', at session or system level
===
SQL_ID 62mars8u2ysj1, child number 0
-------------------------------------
update sl_szy_mwr_intra .WR_MP_HOURW_R set MP_CD =
'2201050002001', SPE_REG_DATA = '0', HOUR_W = 1368.0,
DT = to_date('06-05-2015 06:00:00', 'dd-mm-yyyy hh24:mi:ss')
where MP_CD = '2201050002001' and DT = to_date('06-05-2015
06:00:00', 'dd-mm-yyyy hh24:mi:ss')
Plan hash value: 234794540
首先考慮oracle是否沒有搜集表的統計信息。查看屬性發現表和索引的統計都較新(oracle 基本上一個小時會收集一次,以保證執行計劃是最優的)。
通過hint 處理強制走索引:/*+ INDEX(WR_MP_HOURW_R WR_MP_HOURW_R_PRI)*/ 。發現走了INDEX SKIP SCAN。
此時說明復合索引有一個字段oracle認為不在條件中或者是經過了轉換。使得oracle只走了復合索引的其中一個字段。
由于我們的update條件是復合索引的兩個字段都在where條件里面,所以很大可能是發生了字段類型轉換。
其實從上文件指出計劃中也可以發現,出現類型轉換 INTERNAL_FUNCTION("DT") 。
如果oracle在索引字段發現有類型轉換(如數據庫是date,但是傳入的是timestemp)oralce將不走索引。
當然如果是復合索引,oracle有可能會走 INDEX FAST FULL SCAN或者INDEX SKIP SCAN。然是如果數據量很大,索引全部掃描也很費時間。必須要走INDEX UNIQUE SCAN才能保證效率。
在發現了發生數據類型轉換后,就只能從程序下手找問題,看是否傳入的值有問題。
通過查詢spring源碼,發現:
類:org.springframework.jdbc.core.StatementCreatorUtils的271行開始為最終調用jdbc驅動來通過PreparedStatement設置值的地方:
第346行,如果我們在傳參數的時候,沒有指定對應在數據庫要映射什么類型是,spring幫我們做了處理,如下
//這里說明我們沒有指定要映射到數據庫的什么類型
else if (sqlType == SqlTypeValue.TYPE_UNKNOWN)
{

if (isStringValue(inValue.getClass()))
{
ps.setString(paramIndex, inValue.toString());
}
//看這里,所有java.util.Date,java.util.Date,java.sql.Date,java.sql.Timestamp都被用了ps.setTimestamp處理了,這就是根本原因

else if (isDateValue(inValue.getClass()))
{
ps.setTimestamp(paramIndex, new java.sql.Timestamp(((java.util.Date) inValue).getTime()));
}

else if (inValue instanceof Calendar)
{
Calendar cal = (Calendar) inValue;
ps.setTimestamp(paramIndex, new java.sql.Timestamp(cal.getTime().getTime()), cal);
}

else
{
// Fall back to generic setObject call without SQL type specified.
ps.setObject(paramIndex, inValue);
}
}


private static boolean isDateValue(Class inValueType)
{
return (java.util.Date.class.isAssignableFrom(inValueType) &&
!(java.sql.Date.class.isAssignableFrom(inValueType) ||
java.util.Date.class.isAssignableFrom(inValueType) ||
java.sql.Timestamp.class.isAssignableFrom(inValueType)));
}
找到問題之后如何解決:
spring為我們提供了SqlParameterValue或者SqlParameter供我們包裝:
如果字段是date類型,我們傳入java.util.date 此時需要封裝成new SqlParameterValue(Types.TIME, value);
當然這里不能用Types.DATE 因為如果用Types.DATE最終會被轉換后為java.sql.date,將會丟失時分秒。
如果字段是timestemp類型,我們傳入java.util.date, 此時需要封裝成new SqlParameterValue(Types.TIMESTAMP, value);
這樣最終oracle就不會出現數據類型轉換。
修改之后再查詢執行計劃,oracle 順利的走了INDEX UNIQUE SCAN。效率立即從更新一條好幾秒變為瞬時。具體計劃如下:
SQL_ID 62mars8u2ysj1, child number 0
-------------------------------------
update sl_szy_mwr_intra .WR_MP_HOURW_R set MP_CD =
'2201050002001', SPE_REG_DATA = '0', HOUR_W = 1368.0,
DT = to_date('06-05-2015 06:00:00', 'dd-mm-yyyy hh24:mi:ss')
where MP_CD = '2201050002001' and DT = to_date('06-05-2015
06:00:00', 'dd-mm-yyyy hh24:mi:ss')
Plan hash value: 234794540
-----------------------------------------------------------------------------------------
| Id | Operation | Name | E-Rows |E-Bytes| Cost (%CPU)| E-Time |
-----------------------------------------------------------------------------------------
| 0 | UPDATE STATEMENT | | | | 3 (100)| |
| 1 | UPDATE | WR_MP_HOURW_R | | | | |
|* 2 | INDEX UNIQUE SCAN| WR_MP_HOURW_R_PRI | 1 | 24 | 2 (0)| 00:00:01 |
-----------------------------------------------------------------------------------------
Query Block Name / Object Alias (identified by operation id):
-------------------------------------------------------------
1 - UPD$1
2 - UPD$1 / WR_MP_HOURW_R@UPD$1
Outline Data
-------------

/**//*+
BEGIN_OUTLINE_DATA
IGNORE_OPTIM_EMBEDDED_HINTS
OPTIMIZER_FEATURES_ENABLE('11.2.0.3')
DB_VERSION('11.2.0.3')
ALL_ROWS
OUTLINE_LEAF(@"UPD$1")
INDEX(@"UPD$1" "WR_MP_HOURW_R"@"UPD$1" ("WR_MP_HOURW_R"."DT"
"WR_MP_HOURW_R"."MP_CD"))
END_OUTLINE_DATA
*/
Predicate Information (identified by operation id):
---------------------------------------------------
2 - access("DT"=TO_DATE(' 2015-05-06 06:00:00', 'syyyy-mm-dd hh24:mi:ss') AND
"MP_CD"='2201050002001')
Column Projection Information (identified by operation id):
-----------------------------------------------------------
2 - (upd=2,3,4,5; cmp=2,3; cpy=2,3) "WR_MP_HOURW_R".ROWID[ROWID,10],
"MP_CD"[CHARACTER,13], "DT"[DATE,7], "HOUR_W"[NUMBER,22],
"SPE_REG_DATA"[CHARACTER,1]
Note
-----
- Warning: basic plan statistics not available. These are only collected when:
* hint 'gather_plan_statistics' is used for the statement or
* parameter 'statistics_level' is set to 'ALL', at session or system level

~end~