MySQL千万大表优化实践

2024-09-06 20:32

本文主要是介绍MySQL千万大表优化实践,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

点击上方蓝色字体,选择“设为星标

回复”资源“获取更多资源

大数据技术与架构

点击右侧关注,大数据开发领域最强公众号!

暴走大数据

点击右侧关注,暴走大数据!

前段时间笔者遇到一个复杂的慢查询,今天有空便进行了整理,以便日后回顾。举一个相似的业务场景的例子。以文章评论为例,查询20191201~20191231日期间发表的经济科技类别的文章,同时需要显示这些文章的热评数目

涉及到的四张表结构如下所示

文章表结构和索引信息如下,文章表中存储了200万数据

评论表结构和索引信息如下,评论表存储了1000万数据

文章分类表结构如下,这张表数据比较少,仅仅存储了300条数据

用户表结构如下,该表存储了100万数据

其中涉及到的慢查询语句如下所示,这个查询语句性能非常慢,执行时间接近60s

SELECTtb_article.`title`,tb_user.`name`,count( 1 ) AS `total`
FROMtb_articleLEFT JOIN tb_cmt ON tb_article.`id` = tb_cmt.`article_id`INNER JOIN tb_user on tb_article.`userid` = tb_user.`id`
WHEREtb_article.`type` IN (SELECT codeFROM tb_categoryWHERE code like '12%' or code like '13%')AND tb_cmt.`upvote` > 100AND tb_cmt.`len` BETWEEN 10 AND 30AND tb_article.`create_time` BETWEEN  '2019-12-01 00:00:00'  AND '2019-12-31 23:59:59'
GROUP BYtb_cmt.`article_id`

使用explain分析慢查询的执行流程

Mysql执行流程如下,首先mysql以tb_category作为驱动表,看到这,有没有感到很奇怪,tb_category在整个查询中只是作为一个子查询存在,tb_category怎么成为驱动表了呢?如果读者了解mysql的in子查询原理的话就很好理解了,mysql会将in查询改写为semi-join关联查询,explain涉及到的start temporary和end temporary用于semi-join的去重。我们可以使用explain extended和show warnings查看mysql改写的的查询语句,mysql改写后的查询语句如下所示

Mysql为什么选择tb_category作为驱动表呢?原因是tb_category的表最小,只有300条数据,mysql查询优化器通常情况下都会以小表作为驱动表。

随后,tb_category和tb_article进行关联计算,关联计算的列是tb_article的type列,mysql使用了tb_article表上的type_time_idx的索引,这个过程mysql使用了Batched Key Access进行了优化以达到减少索引回表查找的IO次数,随后关联tb_cmt表,这次关联中,mysql使用了tb_cmt的article_id_idx字段。经过上述关联,mysql生成了一个结果集,mysql再在结果集上对upvote,type和len字段进行where条件筛选,最后进行了一次group by操作。

优化的核心思路仍然是减少扫描的行数,从上述的explain结果上看,扫描的rows行数好像不是很多,但是tb_category,tb_article,tb_cmt,tb_user四张表关联之后生成的结果集非常的庞大,笔者使用如下代码进行以一次计算

SELECTcount(*)
FROMtb_articleLEFT JOIN tb_cmt ON tb_article.`id` = tb_cmt.`article_id`INNER JOIN tb_user on tb_article.`userid` = tb_user.`id`
WHEREtb_article.`type` IN (SELECT codeFROM tb_categoryWHERE code like '12%' or code like '13%')

结果如下

四张表的关联结果集有611万数据

如果读者了解Mysql关联查询原理的话,读者便会知道mysql的关联查询之后,如果再进行条件筛选是无法使用非驱动表索引的(换一句话讲,mysql关联查询只会使用驱动表的索引进行条件筛选),也就是说下面几个条件都是无法使用索引的

在611万结果集上进行upvote,len,create_time条件筛选和group by操作性能可想而知很慢了。笔者希望在执行关联查询的时候可以尽量多的使用索引,比如upvote_len_idx,create_time_idx索引,所以驱动表一定不能是tb_category。和1000万数据量的tb_cmt表相比,笔者更希望以只有200万数据量的tb_article表作为驱动表。

步骤一:避免semi-join

如果笔者希望以tb_article作为驱动表,那么一定要避免in的关联子查询,因为mysql在执行in关联子查询的时候,会将其转化为semi-join,因为tb_category数据量少,mysql查询优化器会使用tb_category作为驱动表。

避免semi-join的关键是避免in子查询,笔者将上述查询语句拆分为两个查询语句,在应用服务层首先执行如下语句选出经济,科技类型文章的编码

SELECT code
FROM tb_category
WHERE code like '12%' or code like '13%'

然后再将上述结果代入到原来查询中,查询语句修改如下

SELECTtb_article.`title`,tb_user.`name`,count( 1 ) AS `total`
FROMtb_articleLEFT JOIN tb_cmt ON tb_article.`id` = tb_cmt.`article_id`INNER JOIN tb_user on tb_article.`userid` = tb_user.`id`
WHEREtb_article.`type` IN ('1213331','1374609','1389750','1204526','1382565','1239054','1321189','1292666')AND tb_cmt.`upvote` > 100AND tb_cmt.`len` BETWEEN 10 AND 30AND tb_article.`create_time` BETWEEN  '2019-12-01 00:00:00'  AND '2019-12-31 23:59:59'
GROUP BYtb_cmt.`article_id`

优化之后查询耗时18s,性能有了非常大的提升,我们再看一下优化后的explain结果

我们看到,mysql以tb_article作为驱动表,并且查询不再涉及semi-join,达到了当前步骤的优化目的

步骤二:尽力使用索引

当前的查询语句以tb_article作为驱动表,同时使用了tb_article上的type_time_idx索引过滤tb_article表,然后关联tb_cmt表,这个关联过程只会使用tb_cmt一个索引article_id,而tb_cmt存储有1000万数据,即使使用了article_id这个索引,最终会生成一个134万的结果集,在134万的结果集上进行如下条件过滤和group by mysql的性能仍然会非常差。

tb_cmt.`upvote` > 100
tb_cmt.`len` BETWEEN 10 AND 30
GROUP BYtb_cmt.`article_id`

笔者希望tb_article仅仅和热门评论进行关联,扫描的数据就大大减少。利用这个思路笔者重新编写sql语句如下

selecttb_article.`title`,tb_user.`name`,count( 1 ) AS `total`
from tb_articleLEFT JOIN (SELECT article_id  FROM  tb_cmtWHERE tb_cmt.upvote > 100AND tb_cmt.len BETWEEN 10 AND 30) ton t.article_id=tb_article.idINNER JOIN tb_user ON tb_article.userid = tb_article.useridAND tb_article.create_time BETWEEN  '2019-12-01 00:00:00'  AND '2019-12-31 23:59:59'AND tb_article.type IN('1213331','1374609','1389750','1204526','1382565','1239054','1321189','1292666')GROUP BY article_id

为了使用tb_cmt上的upvote_len_idx索引,笔者延迟了tb_cmt关联,先对tb_cmt进行了筛选。虽然这个查询会生成一个临时表t,但是临时表t比较小,数据量不足10万,所以这个临时表也不会造成太大的性能负担。但是tb_cmt的子查询却无法使用upvote_len_idx索引,我们还得对范围查询进行优化

步骤三:范围查询优化

笔者让tb_article和筛选过的评论表即热评表t进行关联,但是发现评论的子查询表仍然不使用upvote_len_idx索引,原因是tb_cmt.upvote > 100是一个范围查询,而tb_cmt.len BETWEEN 10 AND 30也是一个范围查询,mysql不支持松散索引扫描,无法在同一个索引上使用两个范围查询。优化思路是将两个范围查询优化为一个范围查询,将tb_cmt.len BETWEEN 10 AND 30优化为散列值,同时删除原来的upvote_len_idx,创建len_upvote_idx索引,目的是将需要范围扫描的upvote字段置为组合索引的尾部。

优化之后代码如下所示

SELECTtb_article.`title`,tb_user.`name`,count( 1 ) AS `total`
from tb_articleLEFT JOIN (SELECT article_id  FROM  tb_cmtWHERE tb_cmt.upvote > 100AND tb_cmt.len in (10 ,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29, 30 )) ton t.article_id=tb_article.idINNER JOIN tb_user ON tb_user.id = tb_article.useridAND tb_article.create_time BETWEEN  '2019-12-01 00:00:00'  AND '2019-12-31 23:59:59'AND tb_article.type IN('1213331','1374609','1389750','1204526','1382565','1239054','1321189','1292666')GROUP BY article_id

在这一步优化之后,笔者再次执行查询,发现性能变得更差了,原本18秒可以运行结束的查询,现在需要40s。原因是什么呢?因为t表的生成过程完全走在索引上,所以t表的生成过程不是性能瓶颈所在,所以笔者猜测是引入的t表和tb_article表左关联时候性能太差的原因,于是笔者注释掉生成t表的子查询以验证笔者的猜想,注释后的代码如下所示

SELECTtb_article.`title`,tb_user.`name`,count( 1 ) AS `total`
from tb_article
-- 	LEFT JOIN (
-- 		    SELECT article_id  FROM  tb_cmt
-- 		    WHERE tb_cmt.upvote > 100
-- 		    AND tb_cmt.len in (10 ,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29, 30 )
-- 		) t
--    on t.article_id=tb_article.idINNER JOIN tb_user ON tb_user.id = tb_article.useridAND tb_article.create_time BETWEEN  '2019-12-01 00:00:00'  AND '2019-12-31 23:59:59'AND tb_article.type IN('1213331','1374609','1389750','1204526','1382565','1239054','1321189','1292666')GROUP BY tb_article.id

上述查询耗时5.26秒,验证了笔者的上述猜想,但是笔者也没有太好的办法解决这个问题,笔者在尝试group by优化时意外找到了优化方案

步骤四 group by优化

仔细观察这个sql语句,我们可以发现GROUP BY这个操作既可以放在临时表t中,又可以放在关联后的结果集上进行,我们如何选择呢?group by无法使用索引,只能使用临时表,所以我们应该让需要被group by的数据尽量的少,而tb_article和tb_cmt是左关联,所以应该将group by操作放在tb_cmt子查询内部进行。除此之外,group by 优化还有一个小技巧,mysql在执行group by的时候,默认会进行排序,在当前业务中,笔者并不需要进行排序,于是笔者在group by 末尾追加order by null ,最终优化的sql结果为

SELECTtb_article.`title`,tb_user.`name`,`total`
from tb_articleLEFT JOIN (SELECT article_id ,count( 1 ) AS `total` FROM  tb_cmtWHERE tb_cmt.upvote > 100AND tb_cmt.len in (10 ,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29, 30 )GROUP BY article_idORDER BY null) ton t.article_id=tb_article.idINNER JOIN tb_user ON tb_user.id = tb_article.useridAND tb_article.create_time BETWEEN  '2019-12-01 00:00:00'  AND '2019-12-31 23:59:59'AND tb_article.type IN('1213331','1374609','1389750','1204526','1382565','1239054','1321189','1292666')

整个查询耗时1.3秒,和原查询耗时60秒相比,已经有了近60倍性能提升。我们再看一下Explain分析

可以看到在将group by放在子查询内部的时候,生成的临时表t好像出现了一个索引<auto_key0>,正是这个key加速了tb_article和临时表t的关联查询。

版权声明:

本文为大数据技术与架构整理,原作者独家授权。未经原作者允许转载追究侵权责任。

编辑|冷眼丶

微信公众号|import_bigdata

欢迎点赞+收藏+转发朋友圈素质三连

文章不错?点个【在看】吧! ????

这篇关于MySQL千万大表优化实践的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!


原文地址:
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.chinasem.cn/article/1143050

相关文章

SQL Server安装时候没有中文选项的解决方法

《SQLServer安装时候没有中文选项的解决方法》用户安装SQLServer时界面全英文,无中文选项,通过修改安装设置中的国家或地区为中文中国,重启安装程序后界面恢复中文,解决了问题,对SQLSe... 你是不是在安装SQL Server时候发现安装界面和别人不同,并且无论如何都没有中文选项?这个问题也

2025版mysql8.0.41 winx64 手动安装详细教程

《2025版mysql8.0.41winx64手动安装详细教程》本文指导Windows系统下MySQL安装配置,包含解压、设置环境变量、my.ini配置、初始化密码获取、服务安装与手动启动等步骤,... 目录一、下载安装包二、配置环境变量三、安装配置四、启动 mysql 服务,修改密码一、下载安装包安装地

MySQL CTE (Common Table Expressions)示例全解析

《MySQLCTE(CommonTableExpressions)示例全解析》MySQL8.0引入CTE,支持递归查询,可创建临时命名结果集,提升复杂查询的可读性与维护性,适用于层次结构数据处... 目录基本语法CTE 主要特点非递归 CTE简单 CTE 示例多 CTE 示例递归 CTE基本递归 CTE 结

在Java中使用OpenCV实践

《在Java中使用OpenCV实践》用户分享了在Java项目中集成OpenCV4.10.0的实践经验,涵盖库简介、Windows安装、依赖配置及灰度图测试,强调其在图像处理领域的多功能性,并计划后续探... 目录前言一 、OpenCV1.简介2.下载与安装3.目录说明二、在Java项目中使用三 、测试1.测

MyBatis-Plus 自动赋值实体字段最佳实践指南

《MyBatis-Plus自动赋值实体字段最佳实践指南》MyBatis-Plus通过@TableField注解与填充策略,实现时间戳、用户信息、逻辑删除等字段的自动填充,减少手动赋值,提升开发效率与... 目录1. MyBATis-Plus 自动赋值概述1.1 适用场景1.2 自动填充的原理1.3 填充策略

MySQL多实例管理如何在一台主机上运行多个mysql

《MySQL多实例管理如何在一台主机上运行多个mysql》文章详解了在Linux主机上通过二进制方式安装MySQL多实例的步骤,涵盖端口配置、数据目录准备、初始化与启动流程,以及排错方法,适用于构建读... 目录一、什么是mysql多实例二、二进制方式安装MySQL1.获取二进制代码包2.安装基础依赖3.清

详解MySQL中JSON数据类型用法及与传统JSON字符串对比

《详解MySQL中JSON数据类型用法及与传统JSON字符串对比》MySQL从5.7版本开始引入了JSON数据类型,专门用于存储JSON格式的数据,本文将为大家简单介绍一下MySQL中JSON数据类型... 目录前言基本用法jsON数据类型 vs 传统JSON字符串1. 存储方式2. 查询方式对比3. 索引

小白也能轻松上手! 路由器设置优化指南

《小白也能轻松上手!路由器设置优化指南》在日常生活中,我们常常会遇到WiFi网速慢的问题,这主要受到三个方面的影响,首要原因是WiFi产品的配置优化不合理,其次是硬件性能的不足,以及宽带线路本身的质... 在数字化时代,网络已成为生活必需品,追剧、游戏、办公、学习都离不开稳定高速的网络。但很多人面对新路由器

在MySQL中实现冷热数据分离的方法及使用场景底层原理解析

《在MySQL中实现冷热数据分离的方法及使用场景底层原理解析》MySQL冷热数据分离通过分表/分区策略、数据归档和索引优化,将频繁访问的热数据与冷数据分开存储,提升查询效率并降低存储成本,适用于高并发... 目录实现冷热数据分离1. 分表策略2. 使用分区表3. 数据归档与迁移在mysql中实现冷热数据分

Olingo分析和实践之EDM 辅助序列化器详解(最佳实践)

《Olingo分析和实践之EDM辅助序列化器详解(最佳实践)》EDM辅助序列化器是ApacheOlingoOData框架中无需完整EDM模型的智能序列化工具,通过运行时类型推断实现灵活数据转换,适用... 目录概念与定义什么是 EDM 辅助序列化器?核心概念设计目标核心特点1. EDM 信息可选2. 智能类