最近,线上的 ETL 数据归档 SQL 发生了点问题,有一个 UPDATE SQL 跑了两天还没跑出来:

 update t_order_record set archive_id = '420a7fe7-4767-45e8-a5f5-72280c192faa', update_time = update_time  where order_id in (select order_id from t_retailer_order_record force index (idx_archive_id) where archive_id = '420a7fe7-4767-45e8-a5f5-72280c192faa')

这个 SQL 其实就是将 t_retailer_order_recordarchive_id420a7fe7-4767-45e8-a5f5-72280c192faa 的所有记录的订单 id order_id,对应的订单表中的记录的 archive_id 也更新为 420a7fe7-4767-45e8-a5f5-72280c192faa 并且更新时间保持不变(因为表上有 update_time 按当前时间更新的触发器)。

对于 SQL 的优化,我们可以使用下面三个工具进行分析:

  1. EXPLAIN:这个是比较浅显的分析,并不会真正执行 SQL,分析出来的可能不够准确详细。但是能发现一些关键问题。
  2. PROFILING: 通过 set profiling = 1 开启的 SQL 执行采样。可以分析 SQL 执行分为哪些阶段,并且每阶段的耗时如何。需要执行并且执行成功 SQL,并且分析出来的阶段不够详细,一般只能通过某些阶段是否存在如何避免这些阶段的出现进行优化(例如避免内存排序的出现等等)。
  3. OPTIMIZER TRACE:详细展示优化器的每一步,需要执行并且执行成功 SQL。MySQL 的优化器由于考虑的因素太多,迭代太多,配置相当复杂,默认的配置在大部分情况没问题,但是在某些特殊情况会有问题,需要我们进行人为干预。

首先,我们针对这个 SQL 进行 EXPLAIN:

+----+--------------------+-------------------------+------------+-------+----------------+----------------+---------+-------+-----------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+--------------------+-------------------------+------------+-------+----------------+----------------+---------+-------+-----------+----------+-------------+
| 1 | UPDATE | t_order_record | NULL | index | NULL | PRIMARY | 8 | NULL | 668618156 | 100.00 | Using where |
| 2 | DEPENDENT SUBQUERY | t_retailer_order_record | NULL | ref | idx_archive_id | idx_archive_id | 195 | const | 1 | 10.00 | Using where |
+----+--------------------+-------------------------+------------+-------+----------------+----------------+---------+-------+-----------+----------+-------------+

发现 t_order_record 的索引使用有问题,这很奇怪:

  1. t_order_record 在 order_id 上面是有索引的,但是这里走的是主键全扫描(主键不是 order_id 而是 id)
  2. 子查询中其实只命中了 3 万多条数据。

一般出现这种情况,肯定又是 SQL 优化器作妖了

这也不能完全怪 SQL 优化器

我们在日常开发与设计表的时候,很难避免会有一些不合理的使用情况,会有很多索引,可能还会出现 large row。这种千奇百怪的情况中,SQL 优化器需要找到最优的方案确实很难。举一个简单的例子:假设我们有一张表,包含主键 id,有 id = 1 的一条记录,一年后,有了 id = 1000000 的一条记录。然后这时我们同时更新了 id = 1 和 id = 1000000 的记录,那么某个通过其他索引但是命中只有 id = 1 和 id = 1000000 的数据很可能不走索引而是主键搜索。因为最近的更新导致这两条数据跑到了同一页上并且在内存中

SQL 优化器考虑了很多这种复杂的情况,能在大部分情况下优化 SQL 为更适应当前情况的,但是由于逻辑过于复杂导致某些简单情况下优化的反而很差,这就需要我们根据 OPTIMIZER TRACE 的结果进行手动优化。

使用测试数据库进行 OPTIMIZER TRACE,先分析索引分析前的步骤是否有问题

由于 Optimizer_trace 需要 SQL 真正执行,但是这个 SQL 执行不出来了。Optimizer_trace 可以分析优化器的全步骤,我们可以先在一个数据量很少的测试环境,看看在进入统计数据分析前(例如分析索引的离散型数据来决定走哪个索引,这个用测试环境模拟不出来,因为数据和线上肯定有差异,即使复制线上的数据也不行,因为数据在哪些页,索引经过怎样的更新,文件结构和线上不同,统计器的信息肯定不会完全一样),SQL 改写转换是否有问题。

执行:

mysql> set session optimizer_trace="enabled=on";
Query OK, 0 rows affected (0.20 sec) mysql> update t_order_record set archive_id = '420a7fe7-4767-45e8-a5f5-72280c192faa', update_time = update_time where order_id in (select order_id from t_retailer_order_record force index (idx_archive_id) where archive_id = '420a7fe7-4767-45e8-a5f5-72280c192faa');
Query OK, 0 rows affected (2.95 sec)
Rows matched: 0 Changed: 0 Warnings: 0 mysql> SELECT trace FROM information_schema.OPTIMIZER_TRACE; steps": [
{
"join_preparation": {
"select#": 2,
"steps": [
{
"expanded_query": "/* select#2 */ select `main`.`t_retailer_order_record`.`order_id` from `main`.`t_retailer_order_record` FORCE INDEX (`idx_archive_id`) where (`main`.`t_retailer_order_record`.`archive_id` = '420a7fe7-4767-45e8-a5f5-72280c192faa')"
},
{
"transformation": {
"select#": 2,
"from": "IN (SELECT)",
"to": "semijoin",
"chosen": false
}
},
{
"transformation": {
"select#": 2,
"from": "IN (SELECT)",
"to": "EXISTS (CORRELATED SELECT)",
"chosen": true,
"evaluating_constant_where_conditions": [
]
}
}
]
}
},
{
"substitute_generated_columns": {
}
},
{
"condition_processing": {
"condition": "WHERE",
## 以下省略

通过 Optimizer_trace 我们发现,优化有问题!将 IN 优化成了 EXISTS。这样导致本来我们想的是使用子查询的每一条记录,去匹配外层订单表的记录,变成了遍历外层订单表的每一条记录,去看是否存在于子查询中,这也解释了为啥 explain 的结果是通过主键遍历订单表的每一条记录进行查询。

这个要改的话,只能改变写法来适应,没法通过关闭优化器选项来实现

于是,我们改写并优化 SQL (使用 JOIN,JOIN 是最接近最容易被优化器理解的编写 SQL 的方式),并且加上了时间条件(我们本身就想只操作 179 天前的数据,这个 archive_id 对应的数据都是 179 天前的),由于订单 id 中本身就带时间(以时间开头,例如 211211094621ord123421 代表 2021 年 12 月 11 日 9 点 46 分 21 秒的一个订单),所以用订单 id 限制时间:

UPDATE t_order_record
JOIN t_retailer_order_record ON t_order_record.order_id = t_retailer_order_record.order_id
SET t_order_record.archive_id = '420a7fe7-4767-45e8-a5f5-72280c192faa',
t_order_record.update_time = t_order_record.update_time
WHERE
t_order_record.order_id < DATE_FORMAT( now() - INTERVAL 179 DAY, '%y%m%d' )
AND t_retailer_order_record.order_id < DATE_FORMAT( now() - INTERVAL 179 DAY, '%y%m%d' )
AND t_retailer_order_record.archive_id = '420a7fe7-4767-45e8-a5f5-72280c192faa'

后续优化经验

如果再遇到这种执行很慢但是实际上更新命中很少数据并且该有的索引都有的情况,可以先在一个数据量很少的测试环境,看看在进入统计数据分析前(例如分析索引的离散型数据来决定走哪个索引,这个用测试环境模拟不出来,因为数据和线上肯定有差异,即使复制线上的数据也不行,因为数据在哪些页,索引经过怎样的更新,文件结构和线上不同,统计器的信息肯定不会完全一样),SQL 改写转换是否有问题。

如果有问题,考虑人为干预手动优化。手动优化的方式包括:

  1. force index 强制用某个索引
  2. 关闭当前会话的 MySQL 优化器的某些选项
  3. 改写 SQL 让优化器更易懂(JOIN 是最容易被 SQL 优化器理解的)

微信搜索“我的编程喵”关注公众号,每日一刷,轻松提升技术,斩获各种offer

由一次 UPDATE 过慢 SQL 优化而总结出的经验的更多相关文章

  1. mysql索引sql优化方法、步骤和经验

    MySQL索引原理及慢查询优化 http://blog.jobbole.com/86594/ 细说mysql索引 https://www.cnblogs.com/chenshishuo/p/50300 ...

  2. 工作中遇到的99%SQL优化,这里都能给你解决方案

    前几篇文章介绍了mysql的底层数据结构和mysql优化的神器explain.后台有些朋友说小强只介绍概念,平时使用还是一脸懵,强烈要求小强来一篇实战sql优化,经过周末两天的整理和总结,sql优化实 ...

  3. sql 优化

    1.选择最有效率的表名顺序(只在基于规则的优化器中有效): oracle的解析器按照从右到左的顺序处理 from 子句中的表名,from子句中写在最后的表(基础表driving table)将被最先处 ...

  4. SQL 优化总结

    SQL 优化总结 (一)SQL Server 关键的内置表.视图 1. sysobjects         SELECT name as '函数名称',xtype as XType  FROM  s ...

  5. (转)SQL 优化原则

    一.问题的提出 在应用系统开发初期,由于开发数据库数据比较少,对于查询SQL语句,复杂视图的的编写等体会不出SQL语句各种写法的性能优劣,但是如果将应用 系统提交实际应用后,随着数据库中数据的增加,系 ...

  6. SQL优化技巧

    我们开发的大部分软件,其基本业务流程都是:采集数据→将数据存储到数据库中→根据业务需求查询相应数据→对数据进行处理→传给前台展示.对整个流程进行分析,可以发现软件大部分的操作时间消耗都花在了数据库相关 ...

  7. 提高SQL查询效率(SQL优化)

    要提高SQL查询效率where语句条件的先后次序应如何写 http://blog.csdn.net/sforiz/article/details/5345359   我们要做到不但会写SQL,还要做到 ...

  8. sql优化建议

    背景:        在北京工作期间,我们做应用开发的和后台数据库的联系非常大,我们经常在一起讨论存储过程或者是sql性能优化的事情来降低应用运行时的时间,提高性能,经过和数据库方面的工程师的一些讨论 ...

  9. oracle sql优化

    整理一下网上所看到sql优化方法 1.使用大写字母书写sql,因为oracle解释器会先将sql语句转换成大写后再解释 2    减少访问数据库的次数,多数情况下一条sql可以达到目的的,就不要使用多 ...

随机推荐

  1. 对于cmd命令的一些常用用法

    cmd命令是当今比较常见的主机命令,对于一些文件的执行和后台数据的控制有着比较有效的规范. 现在就讲几个比较常见的命令. IP查询 按住window+r 进入控制台,在输入cmd命令.在弹出的 cmd ...

  2. Python迭代器生成器与生成式

    Python迭代器生成器与生成式 什么是迭代 迭代是重复反馈过程的活动,其目的通常是为了逼近所需目标或结果.每一次对过程的重复称为一次"迭代",而每一次迭代得到的结果会作为下一次迭 ...

  3. Jmeter BlazeMeter实现web录制

      1. BlazeMeter安装和注册 BlazeMeter是一款与Apache JMeter兼容的chrome插件,采用BlazeMeter可以方便的进行流量录制和脚本生成,作为接口测试脚本编写的 ...

  4. Codeforces 497E - Subsequences Return(矩阵乘法)

    Codeforces 题目传送门 & 洛谷题目传送门 一道还算不错的矩乘 tea 罢,不过做过类似的题应该就比较套路了-- 首先考虑对于一个固定的序列 \(\{a\}\) 怎样求其本质不同的序 ...

  5. Codeforces 1332G - No Monotone Triples(数据结构综合)

    Codeforces 题目传送门 & 洛谷题目传送门 首先打表即可发现对于任意长度 \(\ge 5\) 的序列总存在一个 Monotone triple,证明不会实在不行直接 \(5^5\) ...

  6. R包对植物进行GO,KEGG注释

    1.安装,加载所用到到R包 用BiocManager安装,可同时加载依赖包 source("https://bioconductor.org/biocLite.R") BiocMa ...

  7. composer设置阿里云镜像源

    composer设置阿里云镜像源 1. 首先把默认的源给禁用掉 composer config -g secure-http false 2. 再修改镜像源 这里我使用阿里的源 composer co ...

  8. Macbook pro进入恢复模式以及无法进入恢复模式解决方案

    看网上很多说用Command+R进入恢复模式,但是,大部分都反馈说,此命令并不能进入恢复模式.我自己也尝试发现了同样问题,最终发现解决方案: 问题出在,[是重新启动电脑,而不是关机+按开机键,否则会造 ...

  9. 《Redis设计与实现》知识点目录

    Redis设计与实现 第一部分 数据结构与对象 第二章 简单动态字符串 p8 简单动态字符串SDS 2.1 SDS的定义 p9 每个sds.h/sdshdr结构表示一个SDS值 2.2 SDS与C字符 ...

  10. .NET SAAS 架构与设计 -SqlSugar ORM

    1.数据库设计 常用的Saas分库分为2种类型的库 1.1 基础信息库 主要存组织架构 .权限.字典.用户等 公共信息 性能优化:因为基础信息库是共享的,所以我们可以使用 读写分离,或者二级缓存来进行 ...