千万级用户系统的SQL调优实战

本文涉及的产品
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS PostgreSQL,高可用系列 2核4GB
RDS MySQL Serverless 高可用系列,价值2615元额度,1个月
简介: 某系统需要对特定的大量用户推送一些消息:促销活动让你办卡有个特价商品

1 案例引入


某系统需要对特定的大量用户推送一些消息:


促销活动

让你办卡

有个特价商品

而首先要通过一些条件筛选出这些用户,而该过程很耗时!


日活百万,注册用户千万,而且若还未分库分表,则该DB里的用户表可能就一张,单表就上千万的用户数据。对该运营系统筛选用户的SQL:


SELECT id, name

FROM users

WHERE id IN (

 SELECT user_id

 FROM users_extent_info

 # 查询最近登录过的用户

 WHERE latest_login_time < xx

)


一般存储用户数据的表会分为两张表:


存储用户的核心数据,如id、name、昵称、手机号,即users表

存储用户的一些拓展信息,如家庭住址、兴趣爱好、最近一次登录时间,即users_extent_info表

然后在外层查询,用 IN 子句查询 id 在子查询结果范围里的users表数据,此时该SQL突然会查出很多数据,可能几千、几万、几十万,所以执行此类SQL前,都会先执行count!


SELECT COUNT(id)

FROM users

WHERE id IN (

   SELECT user_id

   FROM users_extent_info

   WHERE latest_login_time < xxxxx

   )


再在内存里再做小批量的批次读数据操作,比如判断:


若结果在1k条内,就一下子读出来

若超过1k条,可通过Limit语句,每次就从该结果集里查1k条,查1000条就做一次批量的消息Push,再查下一批次的1k条数据

但在千万级数据量的大表下,上面SQL竟然耗时几十s!


系统运行时,先Count该结果集有多少数据,再分批查询。然而Count在千万级大表场景下,都要花几十s。其实不同MySQL版本都可能会调整生成执行计划的方式。


通过:


EXPLAIN

SELECT COUNT(id)

FROM users

WHERE id IN (

 SELECT user_id

 FROM users_extent_info

 WHERE latest_login_time < xx

)


如下执行计划是为了调优,在测试环境的单表2w条数据场景。即使5w条数据,当时这SQL都跑了十几s,注意执行计划里的数据量:


| id | select_type | table | type | key | rows | filtered | Extra |

+----+-------------+-------+------------+-------+---------------+----------+---------+---

| 1 | SIMPLE | | ALL | NULL | NULL | 100.00 | NULL |

| 1 | SIMPLE | users | ALL | NULL | 49651 | 10.00 | Using where; Using join buffer(Block Nested Loop) |

| 2 | MATERIALIZED | users_extent_info | range | idx_login_time | 4561 | 100.00 | NULL |


先子查询,users_extent_info使用idx_login_time索引,做range类型的索引范围扫描,查出4561条数据,无额外筛选,所以filtered=100%。


MATERIALIZED:这里把子查询的4561条数据代表的结果集物化成了一个临时表,这个临时表物化会将4561条数据临时落到磁盘文件,这过程很慢!


第二条执行计划


对users表做了全表扫描,扫出49651条数据,Extra=Using join buffer,此处居然在执行join!


执行计划里的第一条


对子查询产出的一个物化临时表做了个全表查询,把里面的数据都扫描了一遍。


为何对该临时表执行全表扫描?让users表的每条数据都和物化临时表里的数据进行join,所以针对users表里的每条数据,只能是去全表扫描一遍物化临时表,从物化临时表里确认哪条数据和他匹配,才能筛选出一条结果。


第二条执行计划的全表扫描结果表明一共扫到49651条,但全表扫描过程中,因为和物化临时表执行join,而物化临时表里就4561条数据,所以最终第二条执行计划的filtered=10%,即最终从users表里也筛选出4000多条数据。


2到底为什么慢?


先执行了子查询查出4561条数据,物化成临时表,接着对users主表全表扫描,扫描过程把每条数据都放到物化临时表里做全表扫描,本质就是在join。


对子查询的结果做了一次物化临时表,落地磁盘,接着还全表扫描users表,每条数据居然还跑到一个无索引的物化临时表,又做了一次全表扫描找匹配数据。


对users表的全表扫描、对users表的每一条数据跑到物化临时表里做全表扫描都很耗时!所以最后结果必然很慢,几乎用不到索引,难道MySQL疯了?


看完执行计划之后,我们可以再执行:


3 show warnings

显示出:


/* select#1 */ select count( d2. users . user_id `) AS COUNT(users.user_id)`

from d2 . users users semi join xxxxxx


注意 semi join ,MySQL在这里生成执行计划时,自动就把一个普通IN子句“优化”成基于semi join来进行 IN+子查询 的操作,那这对users表不就是全表扫描了吗?


对users表里的每条数据,去对物化临时表全表扫描做semi join,无需将users表里的数据真的跟物化临时表里的数据join。只要users表里的一条数据,在物化临时表能找到匹配数据,则users表里的数据就会返回,这就是semi join,用来做筛选。


所以就是semi join和物化临时表导致的慢,那怎么优化?


4 做个实验


SET optimizer_switch='semijoin=off'


关闭半连接优化,再执行EXPLAIN发现恢复为正常状态:


有个SUBQUERY子查询,基于range方式扫描索引,搜索出4561条数据

接着有个PRIMARY类型主查询,直接基于id这个PRIMARY主键索引搜索

然后再把这个SQL语句真实跑一下看看,性能竟然提升几十倍,仅100多ms。

所以,其实反而是MySQL自动执行的semi join半连接优化,导致极差性能,关闭之即可。


生产环境当然不能随意更改这些设置,于是想了多种办法尝试去修改SQL语句的写法,在不影响其语义情况下,尽可能改变SQL语句的结构和格式,最终尝试出如下写法:


SELECT COUNT(id)

FROM users

WHERE (

   id IN (

       SELECT user_id

       FROM users_extent_info

       WHERE latest_login_time < xxxxx)

       OR

   id IN (

       SELECT user_id

       FROM users_extent_info

       WHERE latest_login_time < -1)

)

上述写法下,WHERE语句的OR后面的第二个条件,业务上根本不可能成立,所以不会影响SQL的业务语义,但改变SQL后,执行计划也会变,就不会再semi join优化了,而是常规地用了子查询,主查询也是基于索引。


所以最核心的,还是看懂SQL执行计划,分析慢的原因,尽量避免全表扫描,用上索引!

相关实践学习
每个IT人都想学的“Web应用上云经典架构”实战
本实验从Web应用上云这个最基本的、最普遍的需求出发,帮助IT从业者们通过“阿里云Web应用上云解决方案”,了解一个企业级Web应用上云的常见架构,了解如何构建一个高可用、可扩展的企业级应用架构。
MySQL数据库入门学习
本课程通过最流行的开源数据库MySQL带你了解数据库的世界。 &nbsp; 相关的阿里云产品:云数据库RDS MySQL 版 阿里云关系型数据库RDS(Relational Database Service)是一种稳定可靠、可弹性伸缩的在线数据库服务,提供容灾、备份、恢复、迁移等方面的全套解决方案,彻底解决数据库运维的烦恼。 了解产品详情:&nbsp;https://www.aliyun.com/product/rds/mysql&nbsp;
目录
相关文章
|
3月前
|
SQL 容灾 安全
云时代SQL Server的终极答案:阿里云 RDS SQL Server如何用异地容灾重构系统可靠性
在数字化转型的浪潮中,数据库的高可用性已成为系统稳定性的生命线。作为经历过多次生产事故的资深开发者,肯定深知传统自建SQL Server架构的脆弱性——直到遇见阿里云 RDS SQL Server,其革命性的异地容灾架构彻底改写了游戏规则。
|
8月前
|
SQL 运维 监控
SQL查询太慢?实战讲解YashanDB SQL调优思路
本文是Meetup第十期“调优实战专场”的第二篇技术文章,上一篇《高效查询秘诀,解码YashanDB优化器分组查询优化手段》中,我们揭秘了YashanDB分组查询优化秘诀,本文将通过一个案例,助你快速上手YashanDB慢日志功能,精准定位“慢SQL”后进行优化。
|
SQL 存储 测试技术
SQL在构建系统中的应用:关键步骤与技巧
在构建基于数据库的应用系统时,SQL(Structured Query Language)作为与数据库交互的核心语言,扮演着至关重要的角色
|
7月前
|
SQL 关系型数据库 MySQL
大数据新视界--大数据大厂之MySQL数据库课程设计:MySQL 数据库 SQL 语句调优方法详解(2-1)
本文深入介绍 MySQL 数据库 SQL 语句调优方法。涵盖分析查询执行计划,如使用 EXPLAIN 命令及理解关键指标;优化查询语句结构,包括避免子查询、减少函数使用、合理用索引列及避免 “OR”。还介绍了索引类型知识,如 B 树索引、哈希索引等。结合与 MySQL 数据库课程设计相关文章,强调 SQL 语句调优重要性。为提升数据库性能提供实用方法,适合数据库管理员和开发人员。
|
7月前
|
关系型数据库 MySQL 大数据
大数据新视界--大数据大厂之MySQL 数据库课程设计:MySQL 数据库 SQL 语句调优的进阶策略与实际案例(2-2)
本文延续前篇,深入探讨 MySQL 数据库 SQL 语句调优进阶策略。包括优化索引使用,介绍多种索引类型及避免索引失效等;调整数据库参数,如缓冲池、连接数和日志参数;还有分区表、垂直拆分等其他优化方法。通过实际案例分析展示调优效果。回顾与数据库课程设计相关文章,强调全面认识 MySQL 数据库重要性。为读者提供综合调优指导,确保数据库高效运行。
|
SQL 数据库 UED
SQL性能提升秘籍:5步优化法与10个实战案例
在数据库管理和应用开发中,SQL查询的性能优化至关重要。高效的SQL查询不仅可以提高应用的响应速度,还能降低服务器负载,提升用户体验。本文将分享SQL优化的五大步骤和十个实战案例,帮助构建高效、稳定的数据库应用。
1017 3
|
SQL 存储 数据库
SQL在构建系统中的应用:关键要素与编写技巧
在构建基于数据库的系统时,SQL(Structured Query Language)扮演着至关重要的角色
|
SQL Oracle 关系型数据库
Oracle SQL:了解执行计划和性能调优
Oracle SQL:了解执行计划和性能调优
311 1
|
SQL 缓存 监控
SQL性能提升指南:五大优化策略与十个实战案例
在数据库性能优化的世界里,SQL优化是提升查询效率的关键。一个高效的SQL查询可以显著减少数据库的负载,提高应用响应速度,甚至影响整个系统的稳定性和扩展性。本文将介绍SQL优化的五大步骤,并结合十个实战案例,为你提供一份详尽的性能提升指南。
1168 0