热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

jetbrain下载mysql驱动慢_MySQL千万大表优化实践

点击上方蓝色字体,选择“设为星标”回复”资源“获取更多资源大数据技术与架构点击右侧关注,大数据开发领域最强公众号!暴走大数据点击右侧关注&

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

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

487fd488f659c55faf739f519abbfc37.png

baccb3ef5c662a3b6f35860037f83293.png

大数据技术与架构点击右侧关注,大数据开发领域最强公众号!c5a29e60ad63354fb4b48478dcc20a7b.png

56d8e7e02dcc01ca488e1d05823e08c3.png

暴走大数据点击右侧关注,暴走大数据!968faf24fea25270bf44684c1007f899.png

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

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

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

f200bd760f2372b166dd20c546441808.png
185e11523470e80cd7f77750c84cb15b.png

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

8f4e61fab3af665af9d200b578b4616e.png
e903b04c6d8779cf47a7ade8e838ed70.png

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

67f96da37cdb4e307e63c03a29b72ac8.png

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

85a32a1a1912aaf255291440637228d6.png

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

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

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

b819d49349decc644b202063947b551c.png

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改写后的查询语句如下所示

677154772a03708e82083c9b789885fa.png

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四张表关联之后生成的结果集非常的庞大,笔者使用如下代码进行以一次计算

SELECT
count(*)
FROM
tb_article
LEFT JOIN tb_cmt ON tb_article.`id` = tb_cmt.`article_id`
INNER JOIN tb_user on tb_article.`userid` = tb_user.`id`
WHERE
tb_article.`type` IN (
SELECT code
FROM tb_category
WHERE code like '12%' or code like '13%'
)

结果如下

ce5dd38a068fc451f89d9cf372b03d8e.png

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

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

3062b9d96c2e849b399633f8f00da73b.png

在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%'

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

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

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

78da0cb837ba5bd2b8d6684599bb1626.png

我们看到,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 BY
tb_cmt.`article_id`

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

select
tb_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 BETWEEN 10 AND 30
) t
on t.article_id=tb_article.id
INNER JOIN tb_user ON tb_article.userid = tb_article.userid
AND 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字段置为组合索引的尾部。

优化之后代码如下所示

SELECT
tb_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.id
INNER JOIN tb_user ON tb_user.id = tb_article.userid
AND 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表的子查询以验证笔者的猜想,注释后的代码如下所示

SELECT
tb_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.id
INNER JOIN tb_user ON tb_user.id = tb_article.userid
AND 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结果为

SELECT
tb_article.`title`,
tb_user.`name`,
`total`
from tb_article
LEFT JOIN (
SELECT article_id ,count( 1 ) AS `total` 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 )
GROUP BY article_id
ORDER BY null
) t
on t.article_id=tb_article.id
INNER JOIN tb_user ON tb_user.id = tb_article.userid
AND 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分析

f5f19cf5b0a5a1f29090631b7708eeef.png

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

cdf0477aa7397d5487d05d714da60d72.gif25c0741f8f42748cf0e4526208268ea2.gif

版权声明:

本文为大数据技术与架构整理,原作者独家授权。未经原作者允许转载追究侵权责任。编辑|冷眼丶微信公众号|import_bigdata欢迎点赞+收藏+转发朋友圈素质三连

0025416ec9d9e207297d7fe8c34ac8cb.png

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




推荐阅读
  • 本文介绍了如何使用php限制数据库插入的条数并显示每次插入数据库之间的数据数目,以及避免重复提交的方法。同时还介绍了如何限制某一个数据库用户的并发连接数,以及设置数据库的连接数和连接超时时间的方法。最后提供了一些关于浏览器在线用户数和数据库连接数量比例的参考值。 ... [详细]
  • 本文介绍了如何在MySQL中将零值替换为先前的非零值的方法,包括使用内联查询和更新查询。同时还提供了选择正确值的方法。 ... [详细]
  • Oracle分析函数first_value()和last_value()的用法及原理
    本文介绍了Oracle分析函数first_value()和last_value()的用法和原理,以及在查询销售记录日期和部门中的应用。通过示例和解释,详细说明了first_value()和last_value()的功能和不同之处。同时,对于last_value()的结果出现不一样的情况进行了解释,并提供了理解last_value()默认统计范围的方法。该文对于使用Oracle分析函数的开发人员和数据库管理员具有参考价值。 ... [详细]
  • 本文介绍了一个在线急等问题解决方法,即如何统计数据库中某个字段下的所有数据,并将结果显示在文本框里。作者提到了自己是一个菜鸟,希望能够得到帮助。作者使用的是ACCESS数据库,并且给出了一个例子,希望得到的结果是560。作者还提到自己已经尝试了使用"select sum(字段2) from 表名"的语句,得到的结果是650,但不知道如何得到560。希望能够得到解决方案。 ... [详细]
  • vue使用
    关键词: ... [详细]
  • 基于PgpoolII的PostgreSQL集群安装与配置教程
    本文介绍了基于PgpoolII的PostgreSQL集群的安装与配置教程。Pgpool-II是一个位于PostgreSQL服务器和PostgreSQL数据库客户端之间的中间件,提供了连接池、复制、负载均衡、缓存、看门狗、限制链接等功能,可以用于搭建高可用的PostgreSQL集群。文章详细介绍了通过yum安装Pgpool-II的步骤,并提供了相关的官方参考地址。 ... [详细]
  • 学习SLAM的女生,很酷
    本文介绍了学习SLAM的女生的故事,她们选择SLAM作为研究方向,面临各种学习挑战,但坚持不懈,最终获得成功。文章鼓励未来想走科研道路的女生勇敢追求自己的梦想,同时提到了一位正在英国攻读硕士学位的女生与SLAM结缘的经历。 ... [详细]
  • 生成式对抗网络模型综述摘要生成式对抗网络模型(GAN)是基于深度学习的一种强大的生成模型,可以应用于计算机视觉、自然语言处理、半监督学习等重要领域。生成式对抗网络 ... [详细]
  • 本文介绍了数据库的存储结构及其重要性,强调了关系数据库范例中将逻辑存储与物理存储分开的必要性。通过逻辑结构和物理结构的分离,可以实现对物理存储的重新组织和数据库的迁移,而应用程序不会察觉到任何更改。文章还展示了Oracle数据库的逻辑结构和物理结构,并介绍了表空间的概念和作用。 ... [详细]
  • IhaveconfiguredanactionforaremotenotificationwhenitarrivestomyiOsapp.Iwanttwodiff ... [详细]
  • 在数据分析工作中,我们通常会遇到这样的问题,一个业务部门由若干业务组构成,需要筛选出每个业务组里业绩前N名的业务员。这其实是一个分组排序的 ... [详细]
  • 使用Ubuntu中的Python获取浏览器历史记录原文: ... [详细]
  • 基于事件驱动的并发编程及其消息通信机制的同步与异步、阻塞与非阻塞、IO模型的分类
    本文介绍了基于事件驱动的并发编程中的消息通信机制,包括同步和异步的概念及其区别,阻塞和非阻塞的状态,以及IO模型的分类。同步阻塞IO、同步非阻塞IO、异步阻塞IO和异步非阻塞IO等不同的IO模型被详细解释。这些概念和模型对于理解并发编程中的消息通信和IO操作具有重要意义。 ... [详细]
  • 关键词:Golang, Cookie, 跟踪位置, net/http/cookiejar, package main, golang.org/x/net/publicsuffix, io/ioutil, log, net/http, net/http/cookiejar ... [详细]
  • 本文介绍了Android 7的学习笔记总结,包括最新的移动架构视频、大厂安卓面试真题和项目实战源码讲义。同时还分享了开源的完整内容,并提醒读者在使用FileProvider适配时要注意不同模块的AndroidManfiest.xml中配置的xml文件名必须不同,否则会出现问题。 ... [详细]
author-avatar
陪你浪迹天涯萱萱
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有