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

本文涉及的产品
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS PostgreSQL,集群系列 2核4GB
云数据库 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了。

相关实践学习
如何快速连接云数据库RDS MySQL
本场景介绍如何通过阿里云数据管理服务DMS快速连接云数据库RDS MySQL,然后进行数据表的CRUD操作。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助 &nbsp; &nbsp; 相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
相关文章
|
1月前
|
SQL 数据库 数据安全/隐私保护
数据库数据恢复——sql server数据库被加密的数据恢复案例
SQL server数据库数据故障: SQL server数据库被加密,无法使用。 数据库MDF、LDF、log日志文件名字被篡改。 数据库备份被加密,文件名字被篡改。
|
3月前
|
SQL 关系型数据库 MySQL
MySQL进阶突击系列(07) 她气鼓鼓递来一条SQL | 怎么看执行计划、SQL怎么优化?
在日常研发工作当中,系统性能优化,从大的方面来看主要涉及基础平台优化、业务系统性能优化、数据库优化。面对数据库优化,除了DBA在集群性能、服务器调优需要投入精力,我们研发需要负责业务SQL执行优化。当业务数据量达到一定规模后,SQL执行效率可能就会出现瓶颈,影响系统业务响应。掌握如何判断SQL执行慢、以及如何分析SQL执行计划、优化SQL的技能,在工作中解决SQL性能问题显得非常关键。
|
28天前
|
关系型数据库 MySQL 大数据
大数据新视界--大数据大厂之MySQL 数据库课程设计:MySQL 数据库 SQL 语句调优的进阶策略与实际案例(2-2)
本文延续前篇,深入探讨 MySQL 数据库 SQL 语句调优进阶策略。包括优化索引使用,介绍多种索引类型及避免索引失效等;调整数据库参数,如缓冲池、连接数和日志参数;还有分区表、垂直拆分等其他优化方法。通过实际案例分析展示调优效果。回顾与数据库课程设计相关文章,强调全面认识 MySQL 数据库重要性。为读者提供综合调优指导,确保数据库高效运行。
|
2月前
|
SQL 关系型数据库 MySQL
如何优化SQL查询以提高数据库性能?
这篇文章以生动的比喻介绍了优化SQL查询的重要性及方法。它首先将未优化的SQL查询比作在自助餐厅贪多嚼不烂的行为,强调了只获取必要数据的必要性。接着,文章详细讲解了四种优化策略:**精简选择**(避免使用`SELECT *`)、**专业筛选**(利用`WHERE`缩小范围)、**高效联接**(索引和限制数据量)以及**使用索引**(加速搜索)。此外,还探讨了如何避免N+1查询问题、使用分页限制结果、理解执行计划以及定期维护数据库健康。通过这些技巧,可以显著提升数据库性能,让查询更高效流畅。
|
3月前
|
SQL 关系型数据库 MySQL
基于SQL Server / MySQL进行百万条数据过滤优化方案
对百万级别数据进行高效过滤查询,需要综合使用索引、查询优化、表分区、统计信息和视图等技术手段。通过合理的数据库设计和查询优化,可以显著提升查询性能,确保系统的高效稳定运行。
116 9
|
3月前
|
SQL 数据库
数据库数据恢复—SQL Server报错“错误 823”的数据恢复案例
SQL Server数据库附加数据库过程中比较常见的报错是“错误 823”,附加数据库失败。 如果数据库有备份则只需还原备份即可。但是如果没有备份,备份时间太久,或者其他原因导致备份不可用,那么就需要通过专业手段对数据库进行数据恢复。
|
4月前
|
SQL Oracle 关系型数据库
如何在 Oracle 中配置和使用 SQL Profiles 来优化查询性能?
在 Oracle 数据库中,SQL Profiles 是优化查询性能的工具,通过提供额外统计信息帮助生成更有效的执行计划。配置和使用步骤包括:1. 启用自动 SQL 调优;2. 手动创建 SQL Profile,涉及收集、执行调优任务、查看报告及应用建议;3. 验证效果;4. 使用 `DBA_SQL_PROFILES` 视图管理 Profile。
|
5月前
|
SQL Oracle 数据库
使用访问指导(SQL Access Advisor)优化数据库业务负载
本文介绍了Oracle的SQL访问指导(SQL Access Advisor)的应用场景及其使用方法。访问指导通过分析给定的工作负载,提供索引、物化视图和分区等方面的优化建议,帮助DBA提升数据库性能。具体步骤包括创建访问指导任务、创建工作负载、连接工作负载至访问指导、设置任务参数、运行访问指导、查看和应用优化建议。访问指导不仅针对单条SQL语句,还能综合考虑多条SQL语句的优化效果,为DBA提供全面的决策支持。
136 11
|
4月前
|
SQL 分布式计算 Java
Spark SQL向量化执行引擎框架Gluten-Velox在AArch64使能和优化
本文摘自 Arm China的工程师顾煜祺关于“在 Arm 平台上使用 Native 算子库加速 Spark”的分享,主要内容包括以下四个部分: 1.技术背景 2.算子库构成 3.算子操作优化 4.未来工作
416 0
|
6月前
|
SQL 缓存 监控
大厂面试高频:4 大性能优化策略(数据库、SQL、JVM等)
本文详细解析了数据库、缓存、异步处理和Web性能优化四大策略,系统性能优化必知必备,大厂面试高频。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
大厂面试高频:4 大性能优化策略(数据库、SQL、JVM等)

热门文章

最新文章