问题现象
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. git使用笔记-基础篇

    git使用手册:https://git-scm.com/book/zh/v1/ 一.分支 1.查看所有本地分支 git branch 2.查看所有本地分支和远程分支 git branch -a 3.查 ...

  2. pat1101. Quick Sort (25)

    1101. Quick Sort (25) 时间限制 200 ms 内存限制 65536 kB 代码长度限制 16000 B 判题程序 Standard 作者 CAO, Peng There is a ...

  3. 搭建MHA

    安装MySQL 5.7 yum源的配置文件如下 [mysql57-community] name=MySQL 5.7 Community Server baseurl=http://repo.mysq ...

  4. sass命令

    tip:sass报错解决 通过ruby编译scss时,发现编译报错,内容如下:   Conversion error: Jekyll::Converters::Scss encountered an ...

  5. cf1059D. Nature Reserve(三分)

    题意 题目链接 Sol 欲哭无泪啊qwq....昨晚一定是智息了qwq 说一个和标算不一样做法吧.. 显然\(x\)轴是可以三分的,半径是可以二分的. 恭喜你获得了一个TLE的做法.. 然后第二维的二 ...

  6. Android Studio 小技巧(1):如何导入AAR文件

    1. 导入AAR.JAR文件 File- > New -> New Module 这样子AAR文件就导入了,然后在app中的build.gradle中做如下添加 dependencies ...

  7. C++ VS编译问题--LINK : fatal error LNK1123: 转换到 COFF 期间失败: 文件无效或损坏

    用VS编译时,当出现错误LINK : fatal error LNK1123: 转换到 COFF 期间失败: 文件无效或损坏: 这个问题的解决方案为: 1. 找到项目\xx属性\配置属性\清单工具\输 ...

  8. SSL--Windows下生成OpenSSL自签证书

    :OPenSSL下载地址:https://www.openssl.org/source/ 编译好的OpenSSL下载地址: http://slproweb.com/products/Win32Open ...

  9. Java笔记 —— 方法重载和方法重写

    Java笔记 -- 方法重载和方法重写 h2{ color: #4ABCDE; } a{ text-decoration: none !important; } a:hover{ color: red ...

  10. ArcGIS Desktop中面与面之间空隙填充

    1.前言 再给客户培训过程中被问到这样一个问题,几个面中间有一个空心部分(如下图所示),如何快速绘制中心部分的要素. 2.操作流程 1.打开Editor工具栏,开始编辑操作. 2.点击创建要素按钮,打 ...