使用 iBatis/MyBatis + Oracle 10g 環境,對 DATE 型態欄位 end_date(已建立索引)進行範圍查詢,綁定變數型態為 java.util.Date,預期走 INDEX RANGE SCAN,實際卻出現 TABLE ACCESS FULL。本文深入剖析 JDBC 驅動預設將 setTimestamp 綁定為 TIMESTAMP 型態,觸發隱含 TO_TIMESTAMP(date_col) 導致索引失效的機制,並提供三種解決路徑。
String sql = "SELECT COUNT(*) FROM table1 WHERE end_date >= :1 AND end_date <= :2";
// Java 端:ps.setTimestamp(1, startDate); ps.setTimestamp(2, endDate);
// end_date 欄位型態:DATE,已建立 B-tree 索引
// 實際執行計畫:TABLE ACCESS FULL
DATE 型態,JDBC setTimestamp 綁定為 DATE,索引正常使用。TIMESTAMP(支援奈秒),JDBC 驅動為追求精度,預設將 setTimestamp 綁定為 TIMESTAMP。date_col = :ts_bind,Oracle 必須將 date_col 轉為 TIMESTAMP 才能比較,產生 TO_TIMESTAMP(date_col) = :ts_bind。| 方案 | 適用版本 | 操作步驟 | 優/缺點 |
|---|---|---|---|
| 1. 升級 11g + 新驅動 API | 11gR2+ | 使用 oracle.jdbc.OraclePreparedStatement.setObject(index, date, OracleTypes.DATE) 或 setDate | 徹底解決,需升級資料庫與驅動 |
| 2. 將 DATE 欄位改為 TIMESTAMP | 所有版本 | ALTER TABLE t MODIFY end_date TIMESTAMP(6) | 零程式碼變更,但儲存空間增加、應用相容性需測試 |
| 3. 設定 V8Compatible 連線屬性 | 9i~19c | 連線字串加入 v8Compatible=true 或 Properties.setProperty("v8Compatible", "true") | 無須改程式碼/結構,驅動回退為 DATE 綁定;建議優先採用 |
-- 開啟 SQL Trace 或 EXPLAIN PLAN
EXPLAIN PLAN FOR SELECT COUNT(*) FROM table1 WHERE end_date >= :1 AND end_date <= :2;
SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY);
-- 確認 OUTLINE 顯示 INDEX RANGE SCAN
LocalDateTime + OracleTypes.TIMESTAMP 或 java.time API,欄位定義為 TIMESTAMP,避開歷史包袱。v8Compatible=true 為最低成本方案,上線前務必完整回歸測試日期相關功能。V$SQL 中高耗用 SQL 的 BIND_TYPE,確認無隱含轉換。理解 JDBC 驅動綁定型態預設行為,是避免「明明有索引卻全表掃描」這類效能陷阱的關鍵知識。