oracle 索引字段包含date类型,使用spring jdbc更新时不走索引,而是走table access full的问题

本文分析了在Oracle数据库中针对含有复合主键的大表进行更新操作时出现性能瓶颈的原因,并给出了具体的解决方案。通过调整Spring框架中日期类型的处理方式,避免了类型转换,从而使更新操作能够正确使用复合索引。

转载地址:http://www.blogjava.net/freeman1984/archive/2015/11/15/428197.html

环境
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~

<think>好的,用户的问题是关于Oracle数据库执行计划中出现TABLE ACCESS FULL(全表扫描)而使用索引的原因及解决方法。我需要结合用户提供的引用内容来系统分析这个问题。首先,用户提到的引用[1]提到索引字段类型匹配导致Oracle内部函数转换,从而索引。比如日期类型和传入的间戳类型一致,这可能导致隐式转换,进而无法使用索引。此外,复合索引的情况可能涉及INDEX FAST FULL SCAN等,但效率可能如INDEX UNIQUE SCAN。 引用[2]提到COUNT查询,如果字段允许NULL,而普通索引包含NULL值,优化器可能选择全表扫描而非索引扫描。因为索引中没有NULL值,统计信息可能显示全表扫描更高效,尤其是当大部分数据为NULL。 引用[3]则讨论了全表扫描可能比索引扫描更快的情况,例如当查询需要访问大部分数据块,全表扫描的顺序I/O效率高于索引扫描的随机I/O。对于分区表,如果查询未正确指定分区条件,也可能导致全表扫描。 接下来需要总结常见原因,如类型匹配、统计信息过索引选择性低、查询条件导致索引失效、NULL值问题、隐式转换、全表扫描更优的情况、索引失效或可见、以及优化器参数设置等。然后针对每个原因提供解决方法,比如检查数据类型更新统计信息、使用复合索引、处理NULL值、避免隐式转换等。 还要注意用户提供的示例中,用户可能遇到的是类型转换或NULL值的问题,需要具体分析。确保回答结构清晰,分点列出原因和解决步骤,帮助用户逐步排查问题。</think>### Oracle全表扫描(TABLE ACCESS FULL)原因及解决方法分析 #### 一、常见原因分析 1. **类型匹配导致隐式转换** 当索引字段类型与查询条件类型一致Oracle会进行$INTERNAL_FUNCTION$隐式转换。例如: - 索引字段为$DATE$类型,传入条件为$TIMESTAMP$类型 - 字段定义为$VARCHAR2$,但传入数值型参数 此执行计划显示: ```sql | Id | Operation | Name | | 0 | SELECT STATEMENT | | |* 1 | TABLE ACCESS FULL| EMP | ``` 带$INTERNAL_FUNCTION("DT")$标识符即为类型转换问题[^1]。 2. **统计信息准确** 当表的统计信息过期,优化器可能错误估算索引扫描代价。可通过以下SQL验证: ```sql SELECT LAST_ANALYZED, NUM_ROWS FROM USER_TABLES WHERE TABLE_NAME='YOUR_TABLE'; ``` 3. **低选择性索引** 若索引列重复值过多(如性别字段),优化器认为全表扫描更高效。假设表有10万条数据,索引选择性计算: $$ \text{选择性} = \frac{\text{同值数量}}{\text{总行数}} $$ 当选择性低于$5\%$易出现全表扫描。 4. **NULL值问题** 普通B树索引存储NULL值,当查询包含$WHERE\ col\ IS\ NULL$或COUNT未处理NULL,可能强制全表扫描[^2]。 5. **复合索引未命中前导列** 复合索引$INDEX(A,B,C)$必须使用前导列A作为查询条件,否则可能触发$INDEX SKIP SCAN$或全表扫描。 #### 二、诊断步骤 1. **查看执行计划细节** ```sql EXPLAIN PLAN FOR SELECT * FROM emp WHERE hire_date = SYSDATE; SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY); ``` 重点关注: - $Predicate Information$中的类型转换提示 - $Note$部分的优化器警告 2. **强制使用索引测试** ```sql SELECT /*+ INDEX(emp emp_hire_date_idx) */ * FROM emp WHERE hire_date = SYSDATE ``` 对比强制索引与默认执行计划的执行间 #### 三、解决方案 1. **类型精确匹配** 使用$TO_DATE()$显式转换: ```sql WHERE hire_date = TO_DATE('2023-08-20','YYYY-MM-DD') ``` 2. **更新统计信息** ```sql EXEC DBMS_STATS.GATHER_TABLE_STATS(OWNNAME=>'SCOTT', TABNAME=>'EMP', METHOD_OPT=>'FOR ALL COLUMNS SIZE AUTO'); ``` 3. **处理NULL值索引** - 创建函数索引: ```sql CREATE INDEX emp_comm_notnull_idx ON emp(NVL(comm,0)); ``` - 修改查询条件: ```sql SELECT COUNT(1) FROM emp WHERE comm IS NOT NULL ``` 4. **复合索引优化** 调整索引顺序使其符合查询模式: ```sql DROP INDEX idx_old; CREATE INDEX idx_new ON emp(deptno, job); -- 高频查询字段前置 ``` 5. **使用索引提示(慎用)** ```sql SELECT /*+ INDEX(emp emp_hire_date_idx) */ ... ``` #### 四、特殊场景说明 1. **分区表优化** 当查询未指定分区键,可能出现$PARTITION RANGE ALL$全分区扫描。应添加分区条件: ```sql WHERE sale_date >= TO_DATE('2023-01-01','YYYY-MM-DD') AND sale_date < TO_DATE('2023-02-01','YYYY-MM-DD') ``` 2. **全表扫描更优的情况** 当查询需要访问超过$20\%$的数据块,全表扫描的顺序I/O效率通常高于索引扫描的随机I/O[^3]。 #### 五、验证案例 某表$orders$有date类型索引列$order_date$,执行以下查询: ```sql SELECT * FROM orders WHERE order_date = '2023-08-20'; -- 发生隐式转换 ``` 优化方案: ```sql SELECT * FROM orders WHERE order_date = TO_DATE('2023-08-20','YYYY-MM-DD'); -- 执行计划显示INDEX RANGE SCAN ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值