社区所有版块导航
Python
python开源   Django   Python   DjangoApp   pycharm  
DATA
docker   Elasticsearch  
aigc
aigc   chatgpt  
WEB开发
linux   MongoDB   Redis   DATABASE   NGINX   其他Web框架   web工具   zookeeper   tornado   NoSql   Bootstrap   js   peewee   Git   bottle   IE   MQ   Jquery  
机器学习
机器学习算法  
Python88.com
反馈   公告   社区推广  
产品
短视频  
印度
印度  
Py学习  »  DATABASE

MySQL 上亿数据表优化后,接口不卡了,系统也稳了

鸭哥聊Java • 3 天前 • 13 次点击  

MySQL 真的是一门“老少皆宜”的数据库,用得久了就知道,它最大的优点不是性能,而是皮实耐用。但有时候,这种“皮实”也可能给你带来灾难——尤其当你面对的是一个上亿条数据的大表。

前段时间,我们项目中的一个 MySQL 实例每天凌晨都会报 SLA 告警,说主从延迟严重。查了一圈,发现罪魁祸首竟然是一个自动清理历史数据的定时任务。而这个任务每天都在硬刚上亿条数据的 arrival_record 表,搞得 MySQL 主从都喘不过气。

你没听错,每晚执行的 delete 操作让我们的数据库差点“原地升天”。

慢查询里的“罪魁祸首”

用 pt-query-digest 把一周的 mysql-slow.log 啪一扔,输出结果让我有点上头。慢查询总耗时 25403 秒,最慢的 SQL 执行了 266 秒,平均每条慢查询执行了 5 秒,还平均扫描了 1766 万行数据。

其中,select 和 delete 操作针对的都是一个叫 arrival_record 的表。

我们抓了其中最频繁的 select,看起来很简单:

select count(*) 
from arrival_record 
where product_id=26 
  and receive_time between '2019-03-25 14:00:00' and '2019-03-25 15:00:00' 
  and receive_spend_ms>=0;

但别被这“平平无奇”的 SQL 语句骗了,它单次扫描的数据有时候能飙到 5600 万行,堪比扫一遍某些大厂的员工花名册。

执行计划一看,嘿,好家伙:

id: 1
select_type: SIMPLE
table: arrival_record
type: ref
key: IXFK_arrival_record
rows: 32261320
filtered: 3.70
Extra: Using index condition; Using where

扫了三千多万行!

那这个表长什么样?我也去看了眼表结构,果然没让我失望:

KEY IXFK_arrival_record (product_id, station_no, sequence, receive_time, arrival_time)

一个五字段的复合索引,但 select 的条件只有 product_id 和 receive_time,连 station_no 都没带,那复合索引自然也就派不上太大用场了。

而且 product_id 这字段选择性巨差,基数低得可怜,唯一能救命的 receive_time 又是排在索引的第四位,你说它能快到哪里去?

不甘心就只能背锅,我们决定“动刀”

既然复合索引没用上,我们直接重新设计索引结构。

抓了一波 select 语句的真实 where 条件,发现很多都是这样:

where sequence='2019-03-27 08:40' 
  and product_id=24 
  and station_no='V4205'

OK,这说明 sequence 和 station_no 是高频字段,而且这些字段组合起来的过滤能力还不错。于是我们决定重建复合索引,从原来的:

(product_id, station_no, sequence, receive_time, arrival_time)

变成:

idx_product_id_sequence_station_no (product_id, sequence, station_no)

并且给 receive_time 单独建一个索引:

idx_receive_time (receive_time)

重建索引之前当然不能贸然直接上生产,搞坏了领导第一个找你喝茶。

我们先用 mydumper 把生产表备份出来,压缩之后从 48G 缩到了 1.2G,我真是头一次佩服这工具的压缩能力。然后在测试环境还原、删除旧索引、建新索引,一顿操作猛如虎。

使用 online ddl 和 pt-osc 分别测试重建索引,最终发现 online ddl 更快,比 pt-osc 节省了将近一半时间,给我们上线打了个强心针。

再看执行计划,一秒钟感受到优化的魅力

加了新的索引之后,我立刻跑了同样的 SQL:

explain select count(*) 
from arrival_record 
where product_id=26 
  and receive_time between '2019-03-25 14:00:00' and '2019-03-25 15:00:00' 
  and receive_spend_ms>=0;

结果:

key: idx_receive_time
rows: 291448
Extra: Using index condition; Using where

扫描行数直接从三千万降到二十九万!

这优化效果,简直是脱胎换骨,比中年男人做完体检后突然决定每天跑五公里还要励志。

delete 语句的另一条命:小批量删除

虽然 select 优化了,但 delete 依旧像个憨憨,每晚那几十秒的大删除一来,主从立马开始喘气。我们尝试给 receive_time 加上索引,效果确实好了一些,但单次删除的记录数一多,执行时间还是猛涨。

后来痛定思痛,我们决定彻底改变删除策略,用“小步快跑”的方式解决大问题。

从之前:

delete from arrival_record where receive_time '2019-03-01';

变成现在:

SELECT MAX(idINTO @need_delete_max_id 
FROM arrival_record 
WHERE receive_time '2019-03-01';

DELETE FROM arrival_record 
WHERE id < @need_delete_max_id 
LIMIT  20000;

每次删 20000 条,删完等 0.5 秒,然后继续删下一批。应用层判断 row_count 是否为 0,是就停,不是就继续删。现在的效果是:

  • 每次 delete 执行时间 < 1 秒
  • 再也没有主从延迟告警
  • DBA 的头发也少掉了几根

这才是对大表 delete 最优雅的打开方式。

一点体会

整个过程干下来,我最大的体会就是:MySQL 的问题,索引八成能解决,剩下两成交给“削峰填谷”策略。

而作为开发,我们不能一味依赖 DBA 和运维,尤其是像 Spring Boot + MyBatis Plus 这种 ORM 框架,很容易一不小心就把全表扫给写进代码里。

记得我改完那段 SQL 后,专门在代码里加了点注释,提醒后人一句:




    
// ❗❗❗ 慎重修改该 SQL,数据量非常大,依赖 idx_receive_time 索引!
// 改了别忘去 explain 一下!!!

写代码就像打牌,能赢的前提是你知道手里的牌是什么,哪些好打,哪些别碰。MySQL 里也是一样,写 SQL 之前最好心里清楚索引长什么样。

再啰嗦两句

如果你也是在维护一个数据量爆炸的系统,建议记住这几条:

  1. 慢查询日志一定要开pt-query-digest 会是你最好的朋友。
  2. 复合索引不是越多越好,别让框架自动建了你就用,得看条件能不能覆盖。
  3. delete 别一次性梭哈,你不是在双十一抢手机,数据库也需要喘气。
  4. 主从延迟=命门,主库出事切换到从库,延迟一大,切换失败,掉线出事故分分钟。

写到这里我都忍不住给自己倒杯咖啡,毕竟这场“大表优化之战”,赢得实在不容易。

最后,我为大家打造了一份deepseek的入门到精通教程,完全免费:https://www.songshuhezi.com/deepseek


同时,也可以看我写的这篇文章《DeepSeek满血复活,直接起飞!》来进行本地搭建。

对编程、职场感兴趣的同学,可以链接我,微信:yagebug  拉你进入“程序员交流群”。
🔥鸭哥私藏精品 热门推荐🔥

鸭哥作为一名老码农,整理了全网最全《Java高级架构师资料合集》
资料包含了 《IDEA视频教程》《最全Java面试题库》、最全项目实战源码及视频》及《毕业设计系统源码》总量高达 650GB 。全部免费领取!全面满足各个阶段程序员的学习需求。

Python社区是高质量的Python/Django开发社区
本文地址:http://www.python88.com/topic/180795
 
13 次点击