千万级用户系统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 相关文章推荐
Mysql官方性能测试工具mysqlslap的使用简介
May 21 MySQL
MySQL 数据类型选择原则
May 27 MySQL
SQL实现LeetCode(196.删除重复邮箱)
Aug 07 MySQL
MySQL命令无法输入中文问题的解决方式
Aug 30 MySQL
MySQL连接控制插件介绍
Sep 25 MySQL
MySQL表类型 存储引擎 的选择
Nov 11 MySQL
关于mysql中时间日期类型和字符串类型的选择
Nov 27 MySQL
MySQL图形化管理工具Navicat安装步骤
Dec 04 MySQL
解决Mysql多行子查询的使用及空值问题
Jan 22 MySQL
MySQL提取JSON字段数据实现查询
Apr 22 MySQL
Mysql中@和@@符号的详细使用指南
Jun 05 MySQL
MySQL数据库配置信息查看与修改方法详解
Jun 25 MySQL
解析MySQL索引的作用
Arthas排查Kubernetes中应用频繁挂掉重启异常
Feb 28 #MySQL
一文搞懂MySQL索引页结构
MySQL七大JOIN的具体使用
一文弄懂MySQL索引创建原则
一文了解MySQL二级索引的查询过程
Mysql数据库表中为什么有索引却没有提高查询速度
You might like
便携利器 — TECSUN PL-365简评
2021/03/02 无线电
模仿OSO的论坛(五)
2006/10/09 PHP
推荐一篇入门级的Class文章
2007/03/19 PHP
PHP基础学习小结
2011/04/17 PHP
php 按指定元素值去除数组元素的实现方法
2011/11/04 PHP
php获取网页标题和内容函数(不包含html标签)
2014/02/03 PHP
ThinkPHP连接数据库及主从数据库的设置教程
2014/08/22 PHP
wamp服务器访问php非常缓慢的解决过程
2015/07/01 PHP
php可扩展的验证类实例(可对邮件、手机号、URL等验证)
2015/07/09 PHP
使用JavaScript库还是自己写代码?
2010/01/28 Javascript
最短的IE判断代码
2011/03/13 Javascript
关于JavaScript中string 的replace
2013/04/12 Javascript
30个经典的jQuery代码开发技巧
2014/12/15 Javascript
js实现简单鼠标跟随效果的方法
2015/04/10 Javascript
jQuery选择器用法实例详解
2015/12/17 Javascript
jQuery hover事件简单实现同时绑定2个方法
2016/06/07 Javascript
轻松实现js弹框显示选项
2016/09/13 Javascript
对vux点击事件的优化详解
2018/08/28 Javascript
JS实现点击按钮随机生成可拖动的不同颜色块示例
2019/01/30 Javascript
antdesign-vue结合sortablejs实现两个table相互拖拽排序功能
2021/01/08 Vue.js
基于python的Tkinter实现一个简易计算器
2015/12/31 Python
python之matplotlib学习绘制动态更新图实例代码
2018/01/23 Python
对python:print打印时加u的含义详解
2018/12/15 Python
学生信息管理系统Python面向对象版
2019/01/30 Python
python模拟鼠标点击和键盘输入的操作
2019/08/04 Python
python将print输出的信息保留到日志文件中
2019/09/27 Python
基于Python中的yield表达式介绍
2019/11/19 Python
python爬虫容易学吗
2020/06/02 Python
详解纯CSS3制作的20种loading动效
2017/07/05 HTML / CSS
关于HTML5 Placeholder新标签低版本浏览器下不兼容的问题分析及解决办法
2016/01/27 HTML / CSS
size?荷兰官方网站:英国高级运动鞋精品店
2020/07/24 全球购物
房地产广告词大全
2014/03/19 职场文书
关于教师节的演讲稿
2014/09/04 职场文书
公司地址变更通知
2015/04/25 职场文书
浅谈python数据类型及其操作
2021/05/25 Python
Ubuntu18.04下QT开发Android无法连接设备问题解决实现
2022/06/01 Java/Android