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

  1. 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:

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

发现 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 改写转换是否有问题。

执行:

  1. mysql> set session optimizer_trace="enabled=on";
  2. Query OK, 0 rows affected (0.20 sec)
  3. 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');
  4. Query OK, 0 rows affected (2.95 sec)
  5. Rows matched: 0 Changed: 0 Warnings: 0
  6. mysql> SELECT trace FROM information_schema.OPTIMIZER_TRACE;
  7. steps": [
  8. {
  9. "join_preparation": {
  10. "select#": 2,
  11. "steps": [
  12. {
  13. "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')"
  14. },
  15. {
  16. "transformation": {
  17. "select#": 2,
  18. "from": "IN (SELECT)",
  19. "to": "semijoin",
  20. "chosen": false
  21. }
  22. },
  23. {
  24. "transformation": {
  25. "select#": 2,
  26. "from": "IN (SELECT)",
  27. "to": "EXISTS (CORRELATED SELECT)",
  28. "chosen": true,
  29. "evaluating_constant_where_conditions": [
  30. ]
  31. }
  32. }
  33. ]
  34. }
  35. },
  36. {
  37. "substitute_generated_columns": {
  38. }
  39. },
  40. {
  41. "condition_processing": {
  42. "condition": "WHERE",
  43. ## 以下省略

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

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

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

  1. UPDATE t_order_record
  2. JOIN t_retailer_order_record ON t_order_record.order_id = t_retailer_order_record.order_id
  3. SET t_order_record.archive_id = '420a7fe7-4767-45e8-a5f5-72280c192faa',
  4. t_order_record.update_time = t_order_record.update_time
  5. WHERE
  6. t_order_record.order_id < DATE_FORMAT( now() - INTERVAL 179 DAY, '%y%m%d' )
  7. AND t_retailer_order_record.order_id < DATE_FORMAT( now() - INTERVAL 179 DAY, '%y%m%d' )
  8. 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. 手写HashMap,快手面试官直呼内行!

    手写HashMap?这么狠,面试都卷到这种程度了? 第一次见到这个面试题,是在某个不方便透露姓名的Offer收割机大佬的文章: 这--我当时就麻了,我们都知道HashMap的数据结构是数组+链表+红黑 ...

  2. 【IDEA】字体大小和类型

    字体大小和类型 2020-09-08  09:06:21  by冲冲 1.工具界面的字体 2.代码的字体 注意:如果已经设置颜色主题,则还需要设置颜色主题的字体,才能生效.

  3. SimpleNVR安防监控RTSP/FLV/HLS直播流服务如何分权限添加用户指定通道观看

    背景分析 随着SimpleNVR的用户越来越多,很多客户反馈给了我们很宝贵的简易以及用户体验.在此非常感谢大家对我们的支持.其中很多客户不想把所有的视频直播展现出来,想分权限添加新用户,指定通道让其观 ...

  4. 数值最优化:一阶和二阶优化算法(Pytorch实现)

    1 最优化概论 (1) 最优化的目标 最优化问题指的是找出实数函数的极大值或极小值,该函数称为目标函数.由于定位\(f(x)\)的极大值与找出\(-f(x)\)的极小值等价,在推导计算方式时仅考虑最小 ...

  5. 洛谷 P5331 - [SNOI2019]通信(CDQ 分治优化建图+费用流)

    题面传送门 首先熟悉网络流的同学应该能一眼看出此题的建模方法: 将每个点拆成两个点 \(in_i,out_i\),连一条 \(S\to in_i\),容量为 \(1\) 费用为 \(0\) 的边 连一 ...

  6. SAM 感性瞎扯

    SAM 做题笔记. 这里是 SAM 感性瞎扯. 最近学了后缀自动机(Suffix_Automaton,SAM),深感其巧妙之处,故写文以记之. 部分文字与图片来源于 OI-Wiki,hihoCoder ...

  7. nginx_日志切割脚本

    #!/bin/bash NGINX_LOG=/usr/loca/nginx/logs/access.log RE_LOG=/data/backup/`data +%Y%m%d` echo -e &qu ...

  8. PHP-FPM运行状态的实时查看及监控详解

    https://www.jb51.net/article/97640.htm https://blog.csdn.net/Dr_cokiy/article/details/105580758

  9. jquery时间轴tab切换效果实现结合swiper实现滑动显示效果

    需求:根据时间轴进行tab页面内容切换(时间轴需要滑动查看并选择) 实现思路: 结合swiper插件实现滑动显示效果 根据transform: translateX进行左侧切换效果的实现(具体实现cs ...

  10. python APScheduler模块

    简介 一般来说Celery是python可以执行定时任务, 但是不支持动态添加定时任务 (Django有插件可以动态添加), 而且对于不需要Celery的项目, 就会让项目变得过重. APSchedu ...