mysql慢查询优化(一)

最近对项目中一些mysql慢查询的问题做了一些优化,今天做一个简单的梳理

1.对limit语句对优化

首先来看一个简单的例子,我们有一个定时任务,是每个月的1号凌晨去跑所有用户的月度报告,需要遍历使用者表,这个表数据量达到了810万,不能一次性加载到内存中处理,需要分页处理,于是我找到了下面这条sql语句

SELECT * FROM `role` WHERE virtual_role != 2 ORDER BY id LIMIT 0,10000

这样的分页sql可能大家都有写过,在数据量小的时候不会有什么问题,但是当分页达到百万级的时候,这条sql就会执行的非常慢

SELECT * FROM `role` WHERE virtual_role != 2 ORDER BY id LIMIT 4000000,10000

这条sql执行了8s

SELECT * FROM `role` WHERE virtual_role != 2 ORDER BY id LIMIT 8000000,10000

这条sql的执行时间竟然高达15s,查看执行计划,发现扫描行数达到了8010000行,limit 8000000,10000的意思是扫描8010000行,丢弃之前的8000000行,返回剩下的10000行,所以换种思路,通过主键id去分页,limit始终只扫描10000行,将sql改为

SELECT * FROM `role` WHERE virtual_role != 2 AND  id
> startId ORDER BY id LIMIT 10000

其中,startId为每页最后一条数据的id值
解析:修改前执行计划的type为index,通过索引对表进行了完全扫描,之后引擎又检查了limit后两个参数相加的行数,修改后为range,通过索引对表进行了范围扫描,只扫描需要的行数,虽然引擎检查的行数也比较多,但并没有扫描整个索引,所以比较稳定,速度也快了很多
修改后的效果:不论查到那一页,执行时间稳定在0.05秒,整个任务中这条sql执行了810次,大约节约了1个小时

2.关于MyIsam并发修改的问题

5月11日做了一个用户推送的任务,一共推送三批数据,总量在10万,如下是关于推送的架构图

Created with Raphaël 2.1.2 push_center push_center 定时任务 定时任务 推送表 推送表 查询 微服务调用 修改表中的推送状态

按照运营提供的推送记录,之前推送等量的数据需要3个小时左右,但是按照我们之前对push_center服务的测试显示,单节点可以支撑 350次/s的push速度,目前拥有两个节点,也就是700次/s,推送10万数据只需要3分钟左右,与运营提供的3个小时差距比较大,性能瓶颈出在了定时任务和数据库这里,在定时任务中找到了如下代码

UPDATE pushTable SET is_push = 0 WHERE user_id = 
1812981

乍一看,这条sql没有什么问题,但一页循环2000次,循环50页,这个速度就会很慢了,测试结果显示循环2000次的时间需要4分钟,50页大概需要3个小时20分钟,业务就是将表中所有数据的推送状态修改为已推送,修改一下

UPDATE pushTable SET is_push = 0

去掉for循环,一张表使用一条sql修改表中的所有数据
解析:MyISAM,锁为表级锁,读取操作不会阻塞读,会阻塞写,插入和修改操作是同步的,会阻塞读和写,之前逻辑是pushTable中有多少userId就会循环修改多少次,每一次修改都要等待前一次修改的锁释放,效率很低下
修改后的效果:这次10万次推送只使用了3分钟,性能提升了60倍

3.一些建议

(1).对于字符串的模糊查询,尽量不要在开头模糊,这样会导致索引失效,即使改字段有索引,也不会使用
去年10月份,出现的一次问题,一条sql导致04的v2_user_login_log表锁死,具体sql找不到了,只记得了其中的一部分

SELECT * FROM v2_user_login_log WHERE app_version LIKE '%i%'

这条sql造成了全表扫描,改成如下

SELECT * FROM v2_user_login_log WHERE app_version LIKE 'i%'

用到了app_version的索引
解析:索引的列前缀匹配原则,如果有复杂的文本匹配需求,更好的解决方案是使用ElasticSearch,MySql并不擅长大文本的检索需求
(2).使用 INSERT ON DUPLICATE KEY 或 INSERT IGNORE 来代替 UPDATE,避免 UPDATE 前需要先 SELECT
(3).如果合适,用 GROUP BY 代替 DISTINCT
解析:在有索引的情况下,GROUP BY比DISTINCT性能稍微好一点,但是二者的执行计划完全相同
下面两条sql查询结果相同,但用时不同

SELECT login_version FROM `v2_user_login_profile` GROUP BY login_version

0.002s

SELECT DISTINCT login_version FROM `v2_user_login_profile`

0.01s
(4).最小化你要查询的数据,只获取你需要的数据,通常来说不要使用 *
解析:除了性能方面,使用*可能会在增加表字段的时候,业务代码抛出异常
(5).组合索引,碰到条件查询,索引就会停止,看如下sql

SELECT COUNT(*) FROM `user` WHERE server_time <= '2018-05-19 23:59:59' and `has_device` != 0 and `has_device` != -1

该表索引为has_device_server_time(has_device, server_time),但只会用到has_device字段。

(6).LIMIT M,N 在特定场景下会降低查询效率,有节制使用。
(7).关于delete, 合适的化,truncate table 效率最高,通过主键删除效率也会很高

delete from v2_repeat_check where update_time<1526674802

这条sql用时10s,作用是删除一天之前的数据数据,想想优化方案
(8).InnoDb尽量使用自增主键
1.使用无序主键,插入和删除数据的时候,会造成数据页大量移动数据,影响性能
2.使用自增主键可能的问题,高并发时,会造成AUTO_INCREMENT锁争用,解决方案是修改innodb_autoinc_lock_mode的值,可以在高并发环境下做的更好,具体分析见下下篇文章
(9).索引不是越多越好,因为索引会占用大量的存储空间,插入和删除数据的效率变低,因为要同步更新索引
(10).有时候存储引擎选择的执行计划并不一定是最优的,此时我们可以使用force index(索引名) 来强制使用某个索引,看下面这条sql

SELECT * FROM `body_index` WHERE `role_id`=20831 and (abnormal_flag=0 or abnormal_flag=2) and `is_del`=0 ORDER BY `time` DESC LIMIT 1

执行时间9s
执行计划中选择的是time作为索引,进行全表扫描,显然这样是很浪费时间的,更优的解决方案是,使用roleId索引,避免全表扫描

SELECT * FROM `body_index` FORCE index(role_id) WHERE `role_id`=20831 and (abnormal_flag=0 or abnormal_flag=2) and `is_del`=0 ORDER BY `time` DESC LIMIT 1

执行时间0.5s

SELECT COUNT(*) FROM `role` LEFT JOIN `user` ON role.user_id = user.id WHERE `user`.has_device != 0 AND `user`.has_device != - 1 AND role.server_time < '2018-05-19 23:59:59'

这条sql执行了162s,优化方案?

SELECT count(*) FROM `v2_user_device` where latin_mac <> '00:00:00:00:00:00' and latin_mac <> 'FF:FF:FF:FF:FF:FF'

3s,优化方案?

猜你喜欢

转载自blog.csdn.net/theCrucian/article/details/80384222