问题现象
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 = '' OR ts_user.location = '')
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来进行比较计算,并依此来计算选择率。

实际案例告诉你为什么Oracle不建议使用varchar2来存时间数据的更多相关文章

  1. (转)在oracle中varchar和varchar2有什么区别?

    1.varchar2把所有字符都占两字节处理(一般情况下),varchar只对汉字和全角等字符占两字节,数字,英文字符等都是一个字节: 2.VARCHAR2把空串等同于null处理,而varchar仍 ...

  2. 用JDBC访问ORACLE数据库 关于commit 增快效率 大数据 等的整理

    1.问:用JDBC访问ORACLE数据库,做DELETE操作,能用JAVA多线程实现吗? ORACLE服务器要怎么配?(以下答案来自网络,仅供参考) 答: Oracle有自己的锁机制.就算你开100条 ...

  3. Oracle计算连续天数,计算连续时间,Oracle连续天数统计

    Oracle计算连续天数,计算连续时间,Oracle连续天数统计 >>>>>>>>>>>>>>>>> ...

  4. Oracle数据库部分迁至闪存存储方案

    Oracle数据库部分迁至闪存存储方案 1.实施需求 2.确认迁移表空间信息 3.确认redo信息 4.确认undo信息 5.表空间迁移到闪存 6.redo迁移到闪存 7.undo迁移到闪存 8.备库 ...

  5. 2017/2/6:在oracle中varchar与varchar2的区别与增删改查

    1.varchar2把所有字符都占两字节处理(一般情况下),varchar只对汉字和全角等字符占两字节,数字,英文字符等都是一个字节:2.VARCHAR2把空串等同于null处理,而varchar仍按 ...

  6. oracle中char],varchar,varchar2

    VARCHAR.VARCHAR2.CHAR的区别 1.CHAR的长度是固定的,而VARCHAR2的长度是可以变化的, 比如,存储字符串"abc",对于CHAR (20),表示你存储 ...

  7. Oracle impdp通过network_link不落地方式导入数据

    --Oracle impdp通过network_link不落地方式导入数据 -----------------------------------------------------2014/01/1 ...

  8. 在oracle中varchar和varchar2有什么区别?

    1.varchar2把所有字符都占两字节处理(一般情况下),varchar只对汉字和全角等字符占两字节,数字,英文字符等都是一个字节:2.VARCHAR2把空串等同于null处理,而varchar仍按 ...

  9. Oracle中表列由VARCHAR2类型改成CLOB

    情景 原来表中的列定义成VARCHAR2类型,众所周知,VARCHAR2类型最大支持长度为4000.假设因为业务须要.想把此列转换为CLOB类型,在Oracle中直接通过ALTER语句转换是行不通的. ...

随机推荐

  1. linux 安装dubbo+zookeeper

    dubbo+zookeeper 启动成功 注意:dubbo-admin-2.5.4 不支持java8

  2. Java学生管理系统(连接数据库查询)超详细

    这几天逼着交Java,借鉴各位师傅的做出来这么个简陋的东西,各位大师傅不要笑我.(学都没有学过Java的我,QAQ~) 下面针对的都是SQL Server系列的连接,如果你使用MySQL那么不必看关于 ...

  3. windows 7下安装tomcat6 web服务器

    因为项目中要使用Mondrian提供ROLAP应用,而Mondrian是运行在tomcat上的. 一. 软件获取: http://tomcat.apache.org/ 二. 安装步骤: 运行可执行程序 ...

  4. 【LDAP】LDAP介绍

    原文:http://ldapman.org/articles/intro_to_ldap.html原文作者:Michael Donnelly 什么是LDAP? LDAP的英文全称是Lightweigh ...

  5. PHP用mysql数据库存储session

    大部分使用php的人一旦应用到session都会使用cookie. cookie虽好可是它也会给我们带来一些隐患的. 隐患一:如果客户端机器的cookie一旦因病毒而失效了,那么session也就相当 ...

  6. java.lang.IllegalArgumentException: Page directive: invalid value for import 问题处理

    1.问题说明: 项目原来用的tomcat版本是apache-tomcat-6.0,后来为了安全原因将版本升至apache-tomcat-7.0,发现有的jsp页面出现下面的异常: java.lang. ...

  7. 判断表单中是否含有disabled属性

    我想判断input里面是否有disabled.或者选中未选中的selected  checked 属性时,需要用  prop()  方法,返回的结果是 true 或 false . attr()这个方 ...

  8. 【转】linux之pmap命令!

    原贴:http://tonykorn97.itpub.net/post/6414/249221 linux之pmap命令! ====================================== ...

  9. Xen安装部署和基本原理

    Refer to : http://www.178linux.com/14764 目录:   1. Xen的简介   1.1  Xen的大体结构   1.2 Xen对VM的称呼   1.3 Xen对C ...

  10. 第一次尝试用Open Live Writer写日志

    注册博客园很久了,一直没怎么用,今天登陆了一下,看到这个写日志工具,看着不错,试一下