一次sql改写优化子查询的案例

本文涉及的产品
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS MySQL,高可用系列 2核4GB
简介: 在生产环境中,一个MySQL RDS实例遭遇了高CPU使用率问题,原因是执行了一条复杂的UPDATE SQL语句,该语句涉及一个无法缓存的子查询(UNCACHEABLE SUBQUERY),导致子查询需要针对每一行数据重复执行,极大地影响了性能。SQL语句的目标是更新一行数据,但执行时间长达30秒。优化方法是将子查询转换为内连接形式,优化后的语句执行时间降低到毫秒级别,显著减少了CPU消耗。通过示例数据和执行计划对比,展示了优化前后的时间差异和执行效率的提升。

     项目组反馈一台生产环境阿里的MySQL RDS上,有一条SQL语句导致这台RDS的CPU利用率达到95之上,这条语句实际更新的数据只有一行,执行时间却在30秒左右。由于是生产环境,这里对语句的表名和列名都做一下调整,调整之后的语句如下:

     

update  qx set qx.col_d=qx.col_d + 0.00001 where (qx.col_a,qx.col_b,qx.col_c) = (
  select w.col_a,w.col_b, w.col_c from (
    select (@i:=@i+1) rn, qy.* from qx qy, (select @i:=0) it where qx.col_a=12345 and qy.col_d > 0 order by qy.col_b,qy.col_c desc) w
    where w.rn=1
  )

这条语句里col_a,col_b, col_c是这个表的主键,上面的语句更新的子查询

select w.col_a,w.col_b, w.col_c from (
    select (@i:=@i+1) rn, qy.* from qx qy, (select @i:=0) it where qx.col_a=12345 and qy.col_d > 0 order by qy.col_b,qy.col_c desc) w
    where w.rn=1

     查询出来的行,实际的语句col_a是变量,这里讲col_a改为12345,然后根据col_b,col_c 反向排序后,返回第一行。这里面变量i的使用是为了生成行号,然后再外层查询获取行号为1的值,从而达到获取排序后首行的值的目的。

这个子查询使用了变量,导致了不必要的复杂性。其实,利用MySQL的limit语句,这个子查询完全可以写成更简单的形式。不过,这不是导致这条语句性能问题的主要原因。

下面看一下这条语句的执行计划:

20240416-eb0de9ed-89a2-429d-bf1c-4106bedaf80e.png

熟悉MySQL的DBA一眼就可以看出这个执行计划的问题所在。这条update语句的执行计划中出现了UNCACHEABLE SUNQUERY,关于UNCACHEABLE SUNQUERY子查询,官方参考文档里是这样描述的:

A subquery for which the resul cannot be cached and must be re-evaluated for each row of the outer query。

翻译一下就是:一条不能被缓存的子查询,对于外部查询的每一行,必须重新执行。这个子查询的外部查询是update操作。返回的行数是1509092行,也就是说每次执行这条sql语句,这个子查询就要执行150多万次。因此,这条语句不仅执行时间长,消耗的CPU资源也非常大。

对于这种采用了子查询的update语句,常用的优化方式是将其改成内连接的形式。这次优化也是这样做的。将其改为内连接的方式后,执行时间只有几毫秒,对于cpu的消耗也基本可以忽略了。下面通过另一个表的类似的数据演示一下这种改写及改写后的效果。先看一下这个表的数据:

mysql> select * from lap_times limit 1;
+--------+----------+------+----------+----------+--------------+
| raceId | driverId | lap  | position | time     | milliseconds |
+--------+----------+------+----------+----------+--------------+
|    841 |       20 |    1 |        1 | 1:38.109 |        98109 |
+--------+----------+------+----------+----------+--------------+
1 row in set (0.00 sec)

mysql> select count(*) from lap_times;
+----------+
| count(*) |
+----------+
|  2156704 |
+----------+
1 row in set (2.69 sec)

lap_times表一共有200多万行,表上的索引如下图所示:

20240416-3672e87d-b870-496e-9372-c341c30a6cbe.png

执行update语句,看一下执行时间:

mysql> update lap_times tx set tx.milliseconds = tx.milliseconds+1 where (tx.raceId, tx.driverId, tx.lap) =
    ->   (select t_tmp.raceId, t_tmp.driverId,t_tmp.lap from (
    ->     select (@i:=@i+1) rn, ti.* from lap_times ti, (select @i:=0) it where ti.raceId=1 and ti.milliseconds> 0 order by ti.driverId,ti.lap desc) t_tmp
    ->      where t_tmp.rn=1
    ->   );
Query OK, 4 rows affected, 2 warnings (15.25 sec)
Rows matched: 4  Changed: 4  Warnings: 2

语句时间为15.25秒,语句的执行计划为:

mysql> explain update lap_times tx set tx.milliseconds = tx.milliseconds+1 where (tx.raceId, tx.driverId, tx.lap) =    (select t_tmp.raceId, t_tmp.driverId,t_tmp.lap from (     select (@i:=@i+1) rn, ti.* from lap_times ti, (select @i:=0) it where ti.raceId=1 and ti.milliseconds> 0 order by ti.driverId,ti.lap desc) t_tmp      where t_tmp.rn=1   );
+----+----------------------+------------+------------+--------+-----------------+-------------+---------+-------+---------+----------+----------------+
| id | select_type          | table      | partitions | type   | possible_keys   | key         | key_len | ref   | rows    | filtered | Extra          |
+----+----------------------+------------+------------+--------+-----------------+-------------+---------+-------+---------+----------+----------------+
|  1 | UPDATE               | tx         | NULL       | ALL    | NULL            | NULL        | NULL    | NULL  | 2020504 |   100.00 | Using where    |
|  2 | UNCACHEABLE SUBQUERY | <derived3> | NULL       | ref    | <auto_key0>     | <auto_key0> | 9       | const |      10 |   100.00 | NULL           |
|  3 | DERIVED              | <derived4> | NULL       | system | NULL            | NULL        | NULL    | NULL  |       1 |   100.00 | Using filesort |
|  3 | DERIVED              | ti         | NULL       | ref    | idx_r_d,lap_key | idx_r_d     | 5       | const |    4020 |    33.33 | Using where    |
|  4 | DERIVED              | NULL       | NULL       | NULL   | NULL            | NULL        | NULL    | NULL  |    NULL |     NULL | No tables used |
+----+----------------------+------------+------------+--------+-----------------+-------------+---------+-------+---------+----------+----------------+
5 rows in set, 3 warnings (0.00 sec)

将这个语句改成内连接的方式

update lap_times tx inner join (
    select ty.* from lap_times ty , (select @i:=0) as it where ty.raceId=1 and  ty.milliseconds > 0 order by ty.driverId,ty.lap desc limit 1) tz 
    on tz.driverId=tx.driverId and tz.raceId=tx.raceId and tz.lap=tx.lap 
set tx.milliseconds= tx.milliseconds+1;

由于原来这条语句的内查询返回的只有一行,所以改成内连接的方式语句的执行结果依然是正确的,这条语句的执行时间为:

Query OK, 4 rows affected, 1 warning (0.04 sec)
Rows matched: 4  Changed: 4  Warnings: 1

执行时间只有40毫秒。改写后的执行计划如下:

mysql> explain update lap_times tx inner join (
    ->     select ty.* from lap_times ty , (select @i:=0) as it where ty.raceId=1 and  ty.milliseconds > 0 order by ty.driverId,ty.lap desc limit 1) tz
    ->     on tz.driverId=tx.driverId and tz.raceId=tx.raceId and tz.lap=tx.lap
    -> set tx.milliseconds= tx.milliseconds+1;
+----+-------------+------------+------------+--------+-----------------+---------+---------+-------------------+------+----------+----------------+
| id | select_type | table      | partitions | type   | possible_keys   | key     | key_len | ref               | rows | filtered | Extra          |
+----+-------------+------------+------------+--------+-----------------+---------+---------+-------------------+------+----------+----------------+
|  1 | PRIMARY     | <derived2> | NULL       | system | NULL            | NULL    | NULL    | NULL              |    1 |   100.00 | NULL           |
|  1 | UPDATE      | tx         | NULL       | ref    | idx_r_d,lap_key | lap_key | 15      | const,const,const |    4 |   100.00 | NULL           |
|  2 | DERIVED     | <derived3> | NULL       | system | NULL            | NULL    | NULL    | NULL              |    1 |   100.00 | Using filesort |
|  2 | DERIVED     | ty         | NULL       | ref    | idx_r_d,lap_key | idx_r_d | 5       | const             | 4020 |    33.33 | Using where    |
|  3 | DERIVED     | NULL       | NULL       | NULL   | NULL            | NULL    | NULL    | NULL              | NULL |     NULL | No tables used |
+----+-------------+------------+------------+--------+-----------------+---------+---------+-------------------+------+----------+----------------+
5 rows in set, 2 warnings (0.01 sec)

MySQL对连接顺序进行了优化,原来的子查询成了外部查询,update则成了内部查询,可以利用上索引lap_key了。

相关实践学习
如何在云端创建MySQL数据库
开始实验后,系统会自动创建一台自建MySQL的 源数据库 ECS 实例和一台 目标数据库 RDS。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助 &nbsp; &nbsp; 相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
相关文章
|
2月前
|
SQL 数据库
数据库数据恢复—SQL Server数据库报错“错误823”的数据恢复案例
SQL Server附加数据库出现错误823,附加数据库失败。数据库没有备份,无法通过备份恢复数据库。 SQL Server数据库出现823错误的可能原因有:数据库物理页面损坏、数据库物理页面校验值损坏导致无法识别该页面、断电或者文件系统问题导致页面丢失。
101 12
数据库数据恢复—SQL Server数据库报错“错误823”的数据恢复案例
|
1月前
|
SQL 存储 关系型数据库
如何巧用索引优化SQL语句性能?
本文从索引角度探讨了如何优化MySQL中的SQL语句性能。首先介绍了如何通过查看执行时间和执行计划定位慢SQL,并详细解析了EXPLAIN命令的各个字段含义。接着讲解了索引优化的关键点,包括聚簇索引、索引覆盖、联合索引及最左前缀原则等。最后,通过具体示例展示了索引如何提升查询速度,并提供了三层B+树的存储容量计算方法。通过这些技巧,可以帮助开发者有效提升数据库查询效率。
101 2
|
6天前
|
SQL 缓存 监控
大厂面试高频:4 大性能优化策略(数据库、SQL、JVM等)
本文详细解析了数据库、缓存、异步处理和Web性能优化四大策略,系统性能优化必知必备,大厂面试高频。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
大厂面试高频:4 大性能优化策略(数据库、SQL、JVM等)
|
14天前
|
SQL 存储 缓存
如何优化SQL查询性能?
【10月更文挑战第28天】如何优化SQL查询性能?
59 10
|
13天前
|
SQL 存储 缓存
SQL Server 数据太多如何优化
11种优化方案供你参考,优化 SQL Server 数据库性能得从多个方面着手,包括硬件配置、数据库结构、查询优化、索引管理、分区分表、并行处理等。通过合理的索引、查询优化、数据分区等技术,可以在数据量增大时保持较好的性能。同时,定期进行数据库维护和清理,保证数据库高效运行。
|
27天前
|
SQL 资源调度 分布式计算
如何让SQL跑快一点?(优化指南)
这篇文章主要探讨了如何在阿里云MaxCompute(原ODPS)平台上对SQL任务进行优化,特别是针对大数据处理和分析场景下的性能优化。
|
1月前
|
SQL 监控 数据库
慢SQL对数据库写入性能的影响及优化技巧
在数据库管理系统中,慢SQL(即执行缓慢的SQL语句)不仅会影响查询性能,还可能对数据库的写入性能产生显著的不利影响
|
1月前
|
SQL 关系型数据库 PostgreSQL
遇到SQL 子查询性能很差?其实可以这样优化
遇到SQL 子查询性能很差?其实可以这样优化
87 2
|
1月前
|
SQL 存储 数据库
慢SQL对数据库写入性能的影响及优化技巧
在数据库管理系统中,慢SQL(即执行缓慢的SQL语句)不仅会影响查询性能,还可能对数据库的写入性能产生显著的不利影响
|
1月前
|
SQL 大数据 API
大数据-132 - Flink SQL 基本介绍 与 HelloWorld案例
大数据-132 - Flink SQL 基本介绍 与 HelloWorld案例
45 0