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

本文涉及的产品
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS MySQL,集群版 2核4GB 100GB
推荐场景:
搭建个人博客
云数据库 RDS MySQL,高可用版 2核4GB 50GB
简介: 在生产环境中,一个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
相关文章
|
1月前
|
SQL 监控 关系型数据库
实际应用中监控和诊断SQL语句执行情况的具体案例
实际应用中监控和诊断SQL语句执行情况的具体案例
|
12天前
|
SQL 存储 关系型数据库
不懂索引,简历上都不敢写自己熟悉SQL优化
大家好,我是考哥。今天给大家带来MySQL索引相关核心知识。对MySQL索引的理解甚至比你掌握还重要,索引是优化SQL的前提和基础,我们一步步来先打好地基。当MySQL表数据量不大时,缺少索引对查询性能的影响都不会太大,可能都是0.0几秒;但当表数据量逐日递增时,建立一个合适且优雅的索引就至关重要了。
800 1
不懂索引,简历上都不敢写自己熟悉SQL优化
|
9天前
|
SQL Java 数据库连接
2万字实操案例之在Springboot框架下基于注解用Mybatis开发实现基础操作MySQL之预编译SQL主键返回增删改查
2万字实操案例之在Springboot框架下基于注解用Mybatis开发实现基础操作MySQL之预编译SQL主键返回增删改查
19 2
|
9天前
|
SQL 分布式计算 大数据
MaxCompute产品使用问题之如果oss文件过大,如何在不调整oss源文件大小的情况下优化查询sql
MaxCompute作为一款全面的大数据处理平台,广泛应用于各类大数据分析、数据挖掘、BI及机器学习场景。掌握其核心功能、熟练操作流程、遵循最佳实践,可以帮助用户高效、安全地管理和利用海量数据。以下是一个关于MaxCompute产品使用的合集,涵盖了其核心功能、应用场景、操作流程以及最佳实践等内容。
|
17天前
|
SQL 关系型数据库 MySQL
SQL优化方法有哪些?
【6月更文挑战第16天】SQL优化方法有哪些?
222 5
|
17天前
|
SQL 数据处理 数据库
如何进行SQL查询优化?
【6月更文挑战第16天】如何进行SQL查询优化?
363 3
|
23天前
|
SQL 缓存 监控
sql anywhere 数据库优化
SQL Anywhere数据库优化涉及索引、查询、配置、硬件、维护和应用程序多方面。关键策略包括:确保索引有效且定期优化,使用EXPLAIN计划优化查询,调整数据库缓存设置,升级硬件,定期备份,优化SQL语句并减少数据库访问。实时监控性能并据此调优,但需依据具体应用场景和版本进行适配测试。
|
26天前
|
SQL 关系型数据库 MySQL
MySQL数据库——索引(3)-索引语法(创建索引、查看索引、删除索引、案例演示),SQL性能分析(SQL执行频率,慢查询日志)
MySQL数据库——索引(3)-索引语法(创建索引、查看索引、删除索引、案例演示),SQL性能分析(SQL执行频率,慢查询日志)
24 2
|
5天前
|
SQL 存储 数据库
sql优化提速整理
sql优化提速整理
|
26天前
|
存储 SQL 关系型数据库
MySQL数据库——SQL优化(1/3)-介绍、插入数据、主键优化
MySQL数据库——SQL优化(1/3)-介绍、插入数据、主键优化
237 1