复杂SQL性能优化的剖析(一)(r11笔记第36天)
今天本来是处理一个简单的故障,但是发现是一环套一环,花了我快一天的时间。
开始是早上收到一条报警:
报警内容: CPUutilization is too high
------------------------------------
报警级别: PROBLEM
------------------------------------
监控项目: CPU idle time:59.94 %
------------------------------------
报警时间:2017.01.06-06:35:22
开始也没有在意,准备花几分钟处理完事,但是发现这个坑越来越大。对于问题的处理,我觉得还是能够刨坑问底,你能说服自己,弄明白了,碰到类似的问题就会得心应手。
我发现数据库的负载有了较大的提升,查看快照级别的DB time负载如下:
BEGIN_SNAP END_SNAP SNAPDATE DURATION_MINS DBTIME
---------- ---------- --------------------------------- ----------
19028 19029 06 Jan 2017 00:00 60 104
19029 19030 06 Jan 2017 01:00 60 233
19030 19031 06 Jan 2017 02:00 60 331
19031 19032 06 Jan 2017 03:00 60 409
19032 19033 06 Jan 2017 04:00 60 465
19033 19034 06 Jan 2017 05:00 60 513
19034 19035 06 Jan 2017 06:00 60 538
19035 19036 06 Jan 2017 07:00 59 591
19036 19037 06 Jan 2017 08:00 60 614
19037 19038 06 Jan 2017 09:00 60 622
19038 19039 06 Jan 2017 10:00 60 665
这个情况不容乐观,很快就定位到是一个SQL语句引起的。
可以看到语句的执行计划发生了改变,本来执行2秒的语句,现在执行近5000多秒。这个差距实在是有些大了。

语句的结构如下:
merge into xxxx using
(
select xxxx from h1_first_dev d, ua_td_active_log a
where d.dt = to_date(:1, 'yyyy-mm-dd')
and a.dt = :2
and d.deviceid is not null
and (d.deviceid = a.idfa or d.deviceid = a.mac or
a.idfa =lower(utl_raw.cast_to_raw(dbms_obfuscation_toolkit.md5(input_string => d.deviceid))))
...
union
select xxxx from h1_first_dev d, ua_td_active_log a
where d.dt = to_date(:1, 'yyyy-mm-dd')
and a.dt = :2
and d.deviceid is not null
and (d.deviceid = a.idfa or d.deviceid = a.mac or
and (a.idfa =lower(utl_raw.cast_to_raw(dbms_obfuscation_toolkit.md5(input_string => nvl(case when instr(d.deviceid, ':') > 0 then replace(substr(substr(d.deviceid, 19, 15), 1, 15), chr(2), '') else replace(substr(d.deviceid, 1, 15), chr(2), '') end, 'null')))))
....
)
看起来语句结构也蛮复杂,而且调用了几个大家平时很少见到的包,这个时候就有两个问题需要解释清楚。
- 之前为什么没有这个问题
- 问题的解决方法是什么
查看数据库层面的活跃会话情况,可以看到有大量的会话被阻塞了,而且看阻塞的频率,语句大概是10分钟执行一次。

我对比了变化前后的执行计划情况。
一个重要的变化是两个执行计划的表关联方式不同,左边的效率较差,使用了Nested Loop Join,右边的部分效率较高,使用了Hash Join

当然语句的执行计划很长,我点到为止,重点是如果单单是表关联方式发生变化,这个肯定说明不了Nested Loop Join比Hash Join要好。
简单总结了一下索引的扫描方式,也有了不同的结果。
IDX_H1_FIRST_DT_DEV_DEVICEID(Nested Loop Join) 使用了index range scan
IDX_H1_FIRST_DEV_APPKEY_DT(Hash Join) 使用了 index skip scan
不过这个地方我排查了直方图等信息,没有发现异常,对于NL Join和Hash Join,我可以简单通过如下的数据来论证。
SQL> select count(*)from mbi.h1_first_dev where dt between sysdate-1 and sysdate;
COUNT(*)
----------
3330
所以说这种的情况下,NL Join是完全没有问题的,支持绰绰有余。
那么这个问题我们怎么分析呢,我们分而治之。里面有几个子查询,我们可以拆开来看。
在子查询的执行计划中,我竟然看到了“MERGE JOIN CARTESIAN”的字样。
哪里来的笛卡尔积?
我拿出一个子查询来解释。
select xxxx from h1_first_dev d, ua_td_active_log a
where d.dt = to_date(:1, 'yyyy-mm-dd')
and a.dt = :2
and d.deviceid is not null
and (d.deviceid = a.idfa or d.deviceid = a.mac or
a.idfa =lower(utl_raw.cast_to_raw(dbms_obfuscation_toolkit.md5(input_string => d.deviceid))))
绑定变量值都是日期相关的。
Name Position Type Value
:1 1 VARCHAR2(32) 2017-01-06
:2 2 VARCHAR2(32) 2017-01-06
:3 3 VARCHAR2(32) 2017-01-06
:4 4 VARCHAR2(32) 2017-01-06
可以看出h1_first_dev的字段dt是日期型,ua_td_active_log的dt是字符型。
在这种情况下就是两个结果集的运算了。
本来根据时间条件,可以从两个表里各筛取出很小的一部分数据。

但是两个表的字段类型又不相同,一个date型,一个varchar2,而且没有映射关系,最要命的是后面的条件and (d.deviceid = a.idfa or d.deviceid = a.mac or ...
最后两个表的关联关系被这个条件给彻底破坏了。
所以如此一来,原本3000条左右的数据的关联,硬是给映射成了4千万数据的关联(表里的数据有4千万),想想就是心塞。
我用下面的一个图来表达我的这种心情吧。

而且里面竟然还用到了几个比较少见的包dbms_obfuscation_toolkit utl_raw 导致CPU解析非常繁忙。
查看SQL Monitor报告,基本都被PL/SQL的包给攻占了。

所以这个问题解决起来其实还是要花一些功夫的。我对比模拟测试了一下。取得增量数据,然后运行同样的SQL
SQL> create table H1_FIRST_DEV as select * from mbi.H1_FIRST_DEV where dt=to_date('2017-01-06','yyyy-mm-dd');
SQL> create table UA_TD_ACTIVE_LOG as select * from mbi.UA_TD_ACTIVE_LOG where dt='2017-01-06';
没有任何索引,也没有收集统计信息,同样的语句消耗在1秒内。 SQL> @sqltune1.sql no rows selected Elapsed: 00:00:00.60
而更为奇怪的是返回竟然是0行。
这个时候回头来看开始性能较差的执行计划统计信息,就能理解了。

所以这个问题的解决方案可以稳定执行计划,让dt的过滤优先。或者是重新修改SQL语句,把一些逻辑做精简和改进,比如PL/SQL调用的包体可以通过数据过滤来处理,毕竟是很小的一部分数据。 当然和开发的同学沟通后,我发现问题还没想象的那么简单,因为还有几个更有挑战的SQL要优化,下一篇来谈谈怎么把一个平均执行20秒的核心SQL优化为2秒,关联的表都是千万级别。
- 仿iphone快速导航悬浮球
- WCF Data Service QuickStart
- 仿支付宝手势密码
- android仿iphone的地区选择
- WCF Data Service 的.NET Client 的不支持原生类型服务操作的解决方法
- Elcipse安装gradle插件
- android View层的绘制流程
- RESTful API生命周期管理
- android 的android httpClient详解
- java线程池ThreadPoolExecutor 如何与 AsyncTask() 组合使用
- Volley请求
- SQLServer复制(二)--事务代理作业
- 将图库的图片剪切并保存
- 数据库复制(一)--复制介绍
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法