优化案例 | CASE WHEN进行SQL改写优化

待优化场景
发现SLOW QUERY LOG中有下面这样一条记录:

...
# Query_time: 59.503827  Lock_time: 0.000198  Rows_sent: 641227  Rows_examined: 13442472  Rows_affected: 0
...
select uid,sum(power) powerup from t1 where 
date>='2017-03-31' and 
UNIX_TIMESTAMP(STR_TO_DATE(concat(date,' ',hour),'%Y-%m-%d %H'))>=1490965200 and 
UNIX_TIMESTAMP(STR_TO_DATE(concat(date,' ',hour),'%Y-%m-%d %H'))<1492174801  and 
aType in (1,6,9) group by uid;

实话说,看到这个SQL我也忍不住想骂人啊,究竟是哪个脑残的XX狗设计的?

竟然把日期时间中的 date 和 hour 给独立出来成两列,查询时再合并成一个新的条件,简直无力吐槽。

吐槽归吐槽,该干活还得干活,谁让咱是DBA呢,SQL优化是咱的拿手好戏不是嘛~

SQL优化之路
1、SQL优化思路
不厌其烦地再说一遍SQL优化思路。

想要优化一个SQL,一般来说就是先看执行计划,观察是否尽可能用到索引,

同时要关注预计扫描的行数,

以及是否产生了临时表(Using temporary) 或者

是否需要进行排序(Using filesort),

想办法消除这些情况。

2、SQL性能瓶颈定位
毫无疑问,想要优化,先看表DDL以及执行计划:

CREATE TABLE `t1` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
  `date` date NOT NULL DEFAULT '0000-00-00',
  `hour` char(2) NOT NULL DEFAULT '00',
  `kid` int(4) NOT NULL DEFAULT '0',
  `uid` int(11) NOT NULL DEFAULT '0',
  `aType` tinyint(2) NOT NULL DEFAULT '0',
  `src` tinyint(2) NOT NULL DEFAULT '1',
  `aid` int(11) NOT NULL DEFAULT '1',
  `acount` int(11) NOT NULL DEFAULT '1',
  `power` decimal(20,2) DEFAULT '0.00',
  PRIMARY KEY (`id`,`date`),
  UNIQUE KEY `did` (`date`,`hour`,`kid`,`uid`,`aType`,`src`,`aid`)
) ENGINE=InnoDB AUTO_INCREMENT=50486620 DEFAULT CHARSET=utf8mb4
/*!50500 PARTITION BY RANGE  COLUMNS(`date`)
(PARTITION p20170316 VALUES LESS THAN ('2017-03-17') ENGINE = InnoDB,
 PARTITION p20170317 VALUES LESS THAN ('2017-03-18') ENGINE = InnoDB
...

yejr@imysql.com[myDB]> EXPLAIN select uid,sum(power) powerup from t1 where 
date>='2017-03-31' and 
UNIX_TIMESTAMP(STR_TO_DATE(concat(date,' ',hour),'%Y-%m-%d %H'))>=1490965200 and 
UNIX_TIMESTAMP(STR_TO_DATE(concat(date,' ',hour),'%Y-%m-%d %H'))<1492174801  and 
aType in (1,6,9) group by uid\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: t1
   partitions: p20170324,p20170325,....all partition
         type: ALL
possible_keys: did
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 25005577
     filtered: 15.00
        Extra: Using where; Using temporary; Using filesort

明显的,这个SQL效率非常低,全表扫描、没有索引、有临时表、需要额外排序,什么倒霉催的全赶上了。

3、优化思考
这个SQL是想统计符合条件的power列总和,虽然 date 列已有索引,但WHERE子句中却对 date 列加了函数,而且还是 date 和 hour 两列的组合条件,那就无法用到这个索引了。

还好,有个聪明伶俐的妹子,突发起想(事实上这位妹子本来就擅长做SQL优化的~),可以用 CASE WHEN 方法来改造下SQL,改成像下面这样的:

select uid,sum(powerup+powerup1) from
(
   select uid,
          case when concat(date,' ',hour) >='2017-03-24 13:00' then power else '0' end as powerup,
          case when concat(date,' ',hour) < '2017-03-25 13:00' then power else '0' end as powerup1
   from t1
   where date>='2017-03-24' 
   and   date <'2017-03-25'
   and  aType in (1,6,9)
) a  group by uid;

是不是很有才,直接把这个没办法用到索引的条件给用CASE WHEN来改造了。看看新的SQL执行计划:

*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: t1
   partitions: p20170324
         type: range
possible_keys: did
          key: idx2_date_addRedType
      key_len: 4
          ref: NULL
         rows: 876375
     filtered: 30.00
        Extra: Using index condition; Using temporary; Using filesort

看看这个SQL的执行代价:

+----------------------------+---------+
| Variable_name              | Value   |
+----------------------------+---------+
| Handler_read_first         | 1       |
| Handler_read_key           | 1834590 |
| Handler_read_last          | 0       |
| Handler_read_next          | 1834589 |
| Handler_read_prev          | 0       |
| Handler_read_rnd           | 232276  |
| Handler_read_rnd_next      | 232277  |
+----------------------------+---------+

及其SLOW QUERY LOG记录的信息:

# Query_time: 6.381254  Lock_time: 0.000166  Rows_sent: 232276  Rows_examined: 2299141  Rows_affected: 0
# Bytes_sent: 4237347  Tmp_tables: 1  Tmp_disk_tables: 0  Tmp_table_sizes: 4187168
# InnoDB_trx_id: 0
# QC_Hit: No  Full_scan: No  Full_join: No  Tmp_table: Yes  Tmp_table_on_disk: No
# Filesort: Yes  Filesort_on_disk: No  Merge_passes: 0
#   InnoDB_IO_r_ops: 0  InnoDB_IO_r_bytes: 0  InnoDB_IO_r_wait: 0.000000
#   InnoDB_rec_lock_wait: 0.000000  InnoDB_queue_wait: 0.000000
#   InnoDB_pages_distinct: 9311

看起来还不是太理想啊,虽然不再扫描全表了,但毕竟还是 有临时表 和 额外排序,想办法消除后再对比看下。

有个变化不知道大家注意到没,新的SLOW QUERY LOG记录多了不少信息,这是因为用了Percona分支版本的插件才支持,这个功能确实不错,甚至还能记录Profiling的详细信息,强烈推荐。

我们新建个 uid 列上的索引,看看能除临时表及排序后的代价如何,看看这个的开销会不会更低。

yejr@imysql.com[myDB]> ALTER TABLE t1 ADD INDEX idx_uid(uid);
yejr@imysql.com[myDB]> EXPLAIN select uid,sum(powerup+powerup1) from
(
   select uid,
          case when concat(date,' ',hour) >='2017-03-24 13:00' then power else '0' end as powerup,
          case when concat(date,' ',hour) < '2017-03-25 13:00' then power else '0' end as powerup1
   from t1
   where date>='2017-03-24' 
   and   date <'2017-03-25'
   and  aType in (1,6,9)
) a  group by uid\G

*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: if_date_hour_army_count
   partitions: p20170331,p20170401...
         type: index
possible_keys: did,idx_uid
          key: idx_uid
      key_len: 4
          ref: NULL
         rows: 12701520
     filtered: 15.00
        Extra: Using where

看看添加索引后SQL的执行代价:

+----------------------------+---------+
| Variable_name              | Value   |
+----------------------------+---------+
| Handler_read_first         | 1       |
| Handler_read_key           | 1       |
| Handler_read_last          | 0       |
| Handler_read_next          | 1834589 |
| Handler_read_prev          | 0       |
| Handler_read_rnd           | 0       |
| Handler_read_rnd_next      | 0       |
+----------------------------+---------+

及其SLOW QUERY LOG记录的信息:

# Query_time: 5.772286  Lock_time: 0.000330  Rows_sent: 232276  Rows_examined: 1834589  Rows_affected: 0
# Bytes_sent: 4215071  Tmp_tables: 0  Tmp_disk_tables: 0  Tmp_table_sizes: 0
# InnoDB_trx_id: 0
# QC_Hit: No  Full_scan: Yes  Full_join: No  Tmp_table: No  Tmp_table_on_disk: No
# Filesort: No  Filesort_on_disk: No  Merge_passes: 0
#   InnoDB_IO_r_ops: 0  InnoDB_IO_r_bytes: 0  InnoDB_IO_r_wait: 0.000000
#   InnoDB_rec_lock_wait: 0.000000  InnoDB_queue_wait: 0.000000
#   InnoDB_pages_distinct: 11470

我们注意到,虽然加了 uid 列索引后的SQL扫描的data page更多了,但执行效率其实是更高的,因为消除了 临时表 和 额外排序,这从 Handlerread% 的结果中也能看出来,很显然它的顺序I/O更多,随机I/O更少,所以虽然需要扫描的 data page 更多,实际上效率却是更快的。

后记
再想想这个SQL还有优化空间吗,显然是有的,那就是把数据表重新设计,将date和hour列整合到一起,这样就不用费劲的拼凑条件并且也能用到索引了。

第1章 全局在胸——用工具对SQL整体优化 1 1.1 都有哪些性能工具 1 1.1.1 不同调优场景分析 2 1.1.2 不同场景对应工具 2 1.2 整体性能工具的要点 4 1.2.1 五大性能报表的获取 5 1.2.2 五大报表关注的要点 10 1.3 案例的分享与交流 18 1.3.1 和并行等待有关的案例 18 1.3.2 和热块竞争有关的案例 19 1.3.3 和日志等待有关的案例 20 1.3.4 新疆某系统的前台优化 20 1.3.5 浙江某系统的调优案例 21 1.4 本章总结延伸与习题 21 1.4.1 总结延伸 21 1.4.2 习题训练 23 第2章 风驰电掣——有效缩短SQL优化过程 24 2.1 SQL调优时间都去哪儿了 25 2.1.1 不善于批处理频频忙交互 25 2.1.2 无法抓住主要矛盾瞎折腾 25 2.1.3 未能明确需求目标白费劲 26 2.1.4 没有分析操作难度乱调优 26 2.2 如何缩短SQL调优时间 27 2.2.1 先获取有助调优的数据库整体信息 27 2.2.2 快速获取SQL运行台前信息 27 2.2.3 快速拿到SQL关联幕后信息 28 2.3 从案例看快速SQL调优 29 2.3.1 获取数据库整体的运行情况 29 2.3.2 获取SQL的各种详细信息 29 2.4 本章总结延伸与习题 32 2.4.1 总结延伸 32 2.4.2 习题训练 33 第3章 循规蹈矩——如何读懂SQL执行计划 34 3.1 执行计划分析概述 35 3.1.1 SQL执行计划是什么 35 3.1.2 统计信息用来做什么 36 3.1.3 数据库统计信息的收集 37 3.1.4 数据库的动态采样 37 3.1.5 获取执行计划的方法(6种武器) 40 3.2 读懂执行计划的关键 48 3.2.1 解释经典执行计划方法 49 3.2.2 总结说明 55 3.3 从案例辨别低效SQL 55 3.3.1 从执行计划读出效率 56 3.3.2 执行计划效率总结 60 3.4 本章习题、总结与延伸 60 第4章 运筹帷幄——左右SQL执行计划妙招 62 4.1 控制执行计划的方法综述 63 4.1.1 控制执行计划的意义 63 4.1.2 控制执行计划的思路 64 4.2 从案例探索其方法及意义 65 4.2.1 HINT的思路 65 4.2.2 非HINT方式的执行计划改变 72 4.2.3 执行计划的固定 100 4.3 本章习题、总结与延伸 102 第5章 且慢,感受体系结构让SQL飞 103 5.1 体系结构知识 104 5.1.1 组成 104 5.1.2 原理 104 5.1.3 体会 105 5.2 体系与SQL优化 106 5.2.1 与共享池相关 107 5.2.2 数据缓冲相关 111 5.2.3 日志归档相关 116 5.3 扩展优化案例 118 5.3.1 与共享池相关 118 5.3.2 数据缓冲相关 122 5.3.3 日志归档相关 126 5.4 本章习题、总结与延伸 130 第6章 且慢,体验逻辑结构让SQL飞 132 6.1 逻辑结构 132 6.2 体系细节与SQL优化 133 6.2.1 Block 133 6.2.2 Segment与extent 137 6.2.3 Tablespace 139 6.2.4 rowid 139 6.3 相关优化案例分析 140 6.3.1 块的相关案例 141 6.3.2 段的相关案例 144 6.3.3 表空间的案例 148 6.3.4 rowid 151 6.4 本章习题、总结与延伸 153 第7章 且慢,探寻表的设计让SQL飞 154 7.1 表设计 154 7.1.1 表的设计 155 7.1.2 其他补充 155 7.2 表设计与SQL优化 156 7.2.1 表的设计 156 7.2.2 其他补充 179 7.3 相关优化案例分析 184 7.3.1 分区表相关案例 185 7.3.2 全局临时表案例 190 7.3.3 监控异常的表设计 195 7.3.4 表设计优化相关案例总结 199 7.4 本章习题、总结与延伸 199 第8章 且慢,学习索引如何让SQL飞 200 8.1 索引知识要点概述 201 8.1.1 索引结构的推理 201 8.1.2 索引特性的提炼 204 8.2 索引的SQL优化 206 8.2.1 经典三大特性 207 8.2.2 组合索引选用 217 8.2.3 索引扫描类型的分类与构造 219 8.3 索引相关优化案例 225 8.3.1 三大特性的相关案例 225 8.3.2 组合索引的经典案例 231 8.4 本章习题、总结与延伸 234 第9章 且慢,弄清索引之阻碍让SQL飞 23
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值