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


Posted in MySQL onMarch 03, 2022

用户日活百万级,注册用户千万级,而且若还没有进行分库分表,则该DB里的用户表可能就一张,单表上千万的用户数据。

某系统专门通过各种条件筛选大量用户,接着对那些用户去推送一些消息:

  • 一些促销活动消息
  • 让你办会员卡的消息
  • 告诉你有一个特价商品的消息

通过一些条件筛选出大量用户,针对这些用户做推送,该过程较耗时-筛选用户过程。

用户日活百万级,注册用户千万级,而且若还没有进行分库分表,则该DB里的用户表可能就一张,单表上千万的用户数据。

对运营系统筛选用户的SQL:

SELECT id, name 
FROM users 
WHERE id IN (
  SELECT user_id 
  FROM users_extent_info 
  WHERE latest_login_time < xxxxx
) 

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

  • 存储用户的核心数据,如id、name、昵称、手机号之类的信息,也就是上面SQL语句里的users表
  • 存储用户的一些拓展信息,比如说家庭住址、兴趣爱好、最近一次登录时间之类的,即users_extent_info

有个子查询,里面针对用户的拓展信息表,即users_extent_info查下最近一次登录时间<某个时间点的用户,可以查询最近才登录过的用户,也可查询很长时间未登录的用户,然后给他们发push,无论哪种场景, 该SQL都适用。

然后在外层查询,用id 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
    )

然后内存里做个小批量,多批次读取数据的操作,比如判断如果在1000条以内,那么就一下子读取出来,若超过1000条,可通过LIMIT语句,每次就从该结果集里查1000条数据,查1000条就做次批量PUSH,再查下一波1000条。

就是在千万级数据量大表场景下,上面SQL直接轻松跑出来耗时几十s,不优化不行!

今天咱们继续来看这个千万级用户场景下的运营系统SQL调优案例,上次已经给大家说了一下业务背景 以及SQL,这个SQL就是如下的一个:

SELECT COUNT(id) FROM users WHERE id IN (SELECT user_id FROM 
users_extent_info WHERE latest_login_time < xxxxx)

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

通过:

EXPLAIN 
SELECT COUNT(id) 
FROM users 
WHERE id IN (
  SELECT user_id 
  FROM users_extent_info 
  WHERE latest_login_time < xxxxx
)

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

执行计划里的第三行

先子查询,针对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多条数据。

到底为什么慢

| 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 |

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

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

users表的全表扫描耗时吗?

users表的每一条数据跑到物化临时表里做全表扫描耗时吗?

所以必然非常慢,几乎用不到索引。为什么MySQL会这样呢?

执行完上述SQL的EXPLAIN命令,看到执行计划之后,再执行:

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和物化临时表导致的慢题,那怎么优化?

做个实验

执行:

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后面的第二个条件,根本不可能成立,因为没有数据的latest_login_time<-1,所以那不会影响SQL业务语义,但改变SQL后,执行计划也会变,就没有再semi join优化了,而是常规地用了子查询,主查询也是基于索引,同样达到几百ms 性能优化。

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

到此这篇关于千万级用户系统SQL调优实战分享的文章就介绍到这了,更多相关SQL调优实战内容请搜索三水点靠木以前的文章或继续浏览下面的相关文章希望大家以后多多支持三水点靠木!

MySQL 相关文章推荐
JDBC连接的六步实例代码(与mysql连接)
May 12 MySQL
详解MySQL连接挂死的原因
May 18 MySQL
Mysql实现简易版搜索引擎的示例代码
Aug 30 MySQL
MySQL数据库超时设置配置的方法实例
Oct 15 MySQL
MYSQL 运算符总结
Nov 11 MySQL
MySQL的InnoDB存储引擎的数据页结构详解
Mar 03 MySQL
CentOS 7安装mysql5.7使用XtraBackUp备份工具命令详解
Apr 12 MySQL
MySQL 数据表操作
May 04 MySQL
MySQL 计算连续登录天数
May 11 MySQL
MySQL 语句执行顺序举例解析
Jun 05 MySQL
mysqldump进行数据备份详解
Jul 15 MySQL
MySQL性能指标TPS+QPS+IOPS压测
Aug 05 MySQL
解析MySQL索引的作用
Arthas排查Kubernetes中应用频繁挂掉重启异常
Feb 28 #MySQL
一文搞懂MySQL索引页结构
MySQL七大JOIN的具体使用
一文弄懂MySQL索引创建原则
一文了解MySQL二级索引的查询过程
Mysql数据库表中为什么有索引却没有提高查询速度
You might like
php下的权限算法的实现
2007/04/28 PHP
对text数据类型不支持代码页转换 从: 1252 到: 936
2011/04/23 PHP
PHP 自定义错误处理函数的使用详解
2013/05/10 PHP
基于PHP遍历数组的方法汇总分析
2013/06/08 PHP
php使用正则表达式获取图片url的方法
2015/01/16 PHP
PHP使用适合阅读的格式显示文件大小的方法
2015/03/05 PHP
YiiFramework入门知识点总结(图文教程)
2015/12/28 PHP
PHP编写简单的App接口
2016/08/28 PHP
PHP设计模式之工厂模式实例总结
2017/09/01 PHP
PHP中用Trait封装单例模式的实现
2019/12/18 PHP
一个判断email合法性的函数[非正则]
2008/12/09 Javascript
JavaScript入门教程(8) Location地址对象
2009/01/31 Javascript
JavaScript replace(rgExp,fn)正则替换的用法
2010/03/04 Javascript
你必须知道的Javascript知识点之&quot;单线程事件驱动&quot;的使用
2013/04/23 Javascript
提高jQuery性能的十个诀窍
2013/11/14 Javascript
javascript 数组的定义和数组的长度
2016/06/07 Javascript
Radio 单选JS动态添加的选项onchange事件无效的解决方法
2016/12/12 Javascript
原生js实现轮播图
2017/02/27 Javascript
React中如何引入Angular组件详解
2018/08/09 Javascript
前端路由&amp;webpack基础配置详解
2019/06/10 Javascript
小程序调用微信支付的方法
2019/09/26 Javascript
详解JavaScript之ES5的继承
2020/07/08 Javascript
python数据结构之二叉树的统计与转换实例
2014/04/29 Python
Ubuntu 下 vim 搭建python 环境 配置
2017/06/12 Python
Python数据结构之单链表详解
2017/09/12 Python
详解Python 协程的详细用法使用和例子
2018/06/15 Python
详解Django解决ajax跨域访问问题
2018/08/24 Python
Django2 连接MySQL及model测试实例分析
2019/12/10 Python
详解python如何引用包package
2020/06/07 Python
python爬虫爬取网页数据并解析数据
2020/09/18 Python
python中类与对象之间的关系详解
2020/12/16 Python
副科竞争上岗演讲稿
2014/05/12 职场文书
人大调研汇报材料
2014/08/14 职场文书
党员个人党性分析材料
2014/12/18 职场文书
2015人事行政工作总结范文
2015/05/21 职场文书
只需要12页,掌握撰写一流商业计划书的技巧
2019/05/07 职场文书