作者丨jia-xin

出处:

https://www.cnblogs.com/YangJiaXin/p/10828244.html

“前段时间刚入职一家公司,就遇到了 MySQL 亿级大表优化这事!

图片来自 Pexels

背景

XX 实例(一主一从)xxx 告警中每天凌晨在报 SLA 报警,该报警的意思是存在一定的主从延迟。(若在此时发生主从切换,需要长时间才可以完成切换,要追延迟来保证主从数据的一致性)

XX 实例的慢查询数量最多(执行时间超过 1s 的 SQL 会被记录),XX 应用那方每天晚上在做删除一个月前数据的任务。

分析

使用 pt-query-digest 工具分析最近一周的 mysql-slow.log:

pt-query-digest --since=148h mysql-slow.log | less

结果第一部分:

最近一个星期内,总共记录的慢查询执行花费时间为 25403s,最大的慢 SQL 执行时间为 266s,平均每个慢 SQL 执行时间 5s,平均扫描的行数为 1766 万。

结果第二部分:

select arrival_record 操作记录的慢查询数量最多有 4 万多次,平均响应时间为 4s,delete arrival_record 记录了 6 次,平均响应时间 258s。 

select xxx_record 语句

select arrival_record 慢查询语句都类似于如下所示,where 语句中的参数字段是一样的,传入的参数值不一样:

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\G

select arrival_record 语句在 MySQL 中最多扫描的行数为 5600 万、平均扫描的行数为 172 万,推断由于扫描的行数多导致的执行时间长。

查看执行计划:

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\G;
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: arrival_record
partitions: NULL
type: ref
possible_keys: IXFK_arrival_record
key: IXFK_arrival_record
key_len: 8
ref: const
rows: 32261320
filtered: 3.70
Extra: Using index condition; Using where
1 row in set, 1 warning (0.00 sec)

用到了索引 IXFK_arrival_record,但预计扫描的行数很多有 3000 多万行:

show index from arrival_record;
+----------------+------------+---------------------+--------------+--------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
| Table | Non_unique | Key_name | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment |
+----------------+------------+---------------------+--------------+--------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
| arrival_record | 0 | PRIMARY | 1 | id | A | 107990720 | NULL | NULL | | BTREE | | |
| arrival_record | 1 | IXFK_arrival_record | 1 | product_id | A | 1344 | NULL | NULL | | BTREE | | |
| arrival_record | 1 | IXFK_arrival_record | 2 | station_no | A | 22161 | NULL | NULL | YES | BTREE | | |
| arrival_record | 1 | IXFK_arrival_record | 3 | sequence | A | 77233384 | NULL | NULL | | BTREE | | |
| arrival_record | 1 | IXFK_arrival_record | 4 | receive_time | A | 65854652 | NULL | NULL | YES | BTREE | | |
| arrival_record | 1 | IXFK_arrival_record | 5 | arrival_time | A | 73861904 | NULL | NULL | YES | BTREE | | |
+----------------+------------+---------------------+--------------+--------------+-----------+-------------+----------+--------+------+------------+---------+---------------+show create table arrival_record;
..........
arrival_spend_ms bigint(20) DEFAULT NULL,
total_spend_ms bigint(20) DEFAULT NULL,
PRIMARY KEY (id),
KEY IXFK_arrival_record (product_id,station_no,sequence,receive_time,arrival_time) USING BTREE,
CONSTRAINT FK_arrival_record_product FOREIGN KEY (product_id) REFERENCES product (id) ON DELETE NO ACTION ON UPDATE NO ACTION
) ENGINE=InnoDB AUTO_INCREMENT=614538979 DEFAULT CHARSET=utf8 COLLATE=utf8_bin |

①该表总记录数约 1 亿多条,表上只有一个复合索引,product_id 字段基数很小,选择性不好。

②传入的过滤条件:

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

没有 station_nu 字段,使用不到复合索引 IXFK_arrival_record 的 product_id,station_no,sequence,receive_time 这几个字段。

③根据最左前缀原则,select arrival_record 只用到了复合索引 IXFK_arrival_record 的第一个字段 product_id,而该字段选择性很差,导致扫描的行数很多,执行时间长。

④receive_time 字段的基数大,选择性好,可对该字段单独建立索引,select arrival_record sql 就会使用到该索引。

现在已经知道了在慢查询中记录的 select arrival_record where 语句传入的参数字段有 product_id,receive_time,receive_spend_ms,还想知道对该表的访问有没有通过其他字段来过滤了?

神器 tcpdump 出场的时候到了,使用 tcpdump 抓包一段时间对该表的 select 语句:

tcpdump -i bond0 -s 0 -l -w - dst port 3316 | strings | grep select | egrep -i 'arrival_record' >/tmp/select_arri.log

获取 select 语句中 from 后面的 where 条件语句:

IFS_OLD=$IFS
IFS=$'\n'
for i in `cat /tmp/select_arri.log `;do echo ${i#*'from'}; done | less
IFS=$IFS_OLDarrival_record arrivalrec0_ where arrivalrec0_.sequence='2019-03-27 08:40' and arrivalrec0_.product_id=17 and arrivalrec0_.station_no='56742'
arrival_record arrivalrec0_ where arrivalrec0_.sequence='2019-03-27 08:40' and arrivalrec0_.product_id=22 and arrivalrec0_.station_no='S7100'
arrival_record arrivalrec0_ where arrivalrec0_.sequence='2019-03-27 08:40' and arrivalrec0_.product_id=24 and arrivalrec0_.station_no='V4631'
arrival_record arrivalrec0_ where arrivalrec0_.sequence='2019-03-27 08:40' and arrivalrec0_.product_id=22 and arrivalrec0_.station_no='S9466'
arrival_record arrivalrec0_ where arrivalrec0_.sequence='2019-03-27 08:40' and arrivalrec0_.product_id=24 and arrivalrec0_.station_no='V4205'
arrival_record arrivalrec0_ where arrivalrec0_.sequence='2019-03-27 08:40' and arrivalrec0_.product_id=24 and arrivalrec0_.station_no='V4105'
arrival_record arrivalrec0_ where arrivalrec0_.sequence='2019-03-27 08:40' and arrivalrec0_.product_id=24 and arrivalrec0_.station_no='V4506'
arrival_record arrivalrec0_ where arrivalrec0_.sequence='2019-03-27 08:40' and arrivalrec0_.product_id=24 and arrivalrec0_.station_no='V4617'
arrival_record arrivalrec0_ where arrivalrec0_.sequence='2019-03-27 08:40' and arrivalrec0_.product_id=22 and arrivalrec0_.station_no='S8356'
arrival_record arrivalrec0_ where arrivalrec0_.sequence='2019-03-27 08:40' and arrivalrec0_.product_id=22 and arrivalrec0_.station_no='S8356'

select 该表 where 条件中有 product_id,station_no,sequence 字段,可以使用到复合索引 IXFK_arrival_record 的前三个字段。

综上所示,优化方法为:

  • 删除复合索引 IXFK_arrival_record

  • 建立复合索引 idx_sequence_station_no_product_id

  • 建立单独索引 indx_receive_time

delete xxx_record 语句

该 delete 操作平均扫描行数为 1.1 亿行,平均执行时间是 262s。

delete 语句如下所示,每次记录的慢查询传入的参数值不一样:

delete from arrival_record where receive_time < STR_TO_DATE('2019-02-23', '%Y-%m-%d')\G

执行计划:

explain select * from arrival_record where receive_time < STR_TO_DATE('2019-02-23', '%Y-%m-%d')\G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: arrival_record
partitions: NULL
type: ALL
possible_keys: NULL
key: NULL
key_len: NULL
ref: NULL
rows: 109501508
filtered: 33.33
Extra: Using where
1 row in set, 1 warning (0.00 sec)

该 delete 语句没有使用索引(没有合适的索引可用),走的全表扫描,导致执行时间长。

优化方法也是:建立单独索引 indx_receive_time(receive_time)。

测试

拷贝 arrival_record 表到测试实例上进行删除重新索引操作。

XX 实例 arrival_record 表信息:

du -sh /datas/mysql/data/3316/cq_new_cimiss/arrival_record*
12K    /datas/mysql/data/3316/cq_new_cimiss/arrival_record.frm
48G    /datas/mysql/data/3316/cq_new_cimiss/arrival_record.ibdselect count() from cq_new_cimiss.arrival_record;
+-----------+
| count() |
+-----------+
| 112294946 |
+-----------+
1亿多记录数SELECT
table_name,
CONCAT(FORMAT(SUM(data_length) / 1024 / 1024,2),'M') AS dbdata_size,
CONCAT(FORMAT(SUM(index_length) / 1024 / 1024,2),'M') AS dbindex_size,
CONCAT(FORMAT(SUM(data_length + index_length) / 1024 / 1024 / 1024,2),'G') AS table_size(G),
AVG_ROW_LENGTH,table_rows,update_time
FROM
information_schema.tables
WHERE table_schema = 'cq_new_cimiss' and table_name='arrival_record';+----------------+-------------+--------------+------------+----------------+------------+---------------------+
| table_name | dbdata_size | dbindex_size | table_size(G) | AVG_ROW_LENGTH | table_rows | update_time |
+----------------+-------------+--------------+------------+----------------+------------+---------------------+
| arrival_record | 18,268.02M | 13,868.05M | 31.38G | 175 | 109155053 | 2019-03-26 12:40:17 |
+----------------+-------------+--------------+------------+----------------+------------+---------------------+

磁盘占用空间 48G,MySQL 中该表大小为 31G,存在 17G 左右的碎片,大多由于删除操作造成的。(记录被删除了,空间没有回收)

备份还原该表到新的实例中,删除原来的复合索引,重新添加索引进行测试。

mydumper 并行压缩备份:

 user=rootpasswd=xxxxsocket=/datas/mysql/data/3316/mysqld.sockdb=cq_new_cimisstable_name=arrival_recordbackupdir=/datas/dump_$table_namemkdir -p $backupdirnohup echo `date +%T` && mydumper -u $user -p $passwd -S $socket  -B $db -c  -T $table_name  -o $backupdir  -t 32 -r 2000000 && echo `date +%T` &

并行压缩备份所花时间(52s)和占用空间(1.2G,实际该表占用磁盘空间为 48G,mydumper 并行压缩备份压缩比相当高):

Started dump at: 2019-03-26 12:46:04
........Finished dump at: 2019-03-26 12:46:56du -sh   /datas/dump_arrival_record/
1.2G    /datas/dump_arrival_record/

拷贝 dump 数据到测试节点:

scp -rp /datas/dump_arrival_record root@10.230.124.19:/datas

多线程导入数据:

time myloader -u root -S /datas/mysql/data/3308/mysqld.sock -P 3308 -p root -B test -d /datas/dump_arrival_record -t 32real 126m42.885s
user 1m4.543s
sys 0m4.267s

逻辑导入该表后磁盘占用空间:

du -h -d 1 /datas/mysql/data/3308/test/arrival_record.*
12K /datas/mysql/data/3308/test/arrival_record.frm
30G /datas/mysql/data/3308/test/arrival_record.ibd
没有碎片,和mysql的该表的大小一致
cp -rp /datas/mysql/data/3308 /datas

分别使用 online DDL 和 pt-osc 工具来做删除重建索引操作。

先删除外键,不删除外键,无法删除复合索引,外键列属于复合索引中第一列:

nohup bash /tmp/ddl_index.sh &
2019-04-04-10:41:39 begin stop mysqld_3308
2019-04-04-10:41:41 begin rm -rf datadir and cp -rp datadir_bak
2019-04-04-10:46:53 start mysqld_3308
2019-04-04-10:46:59 online ddl begin
2019-04-04-11:20:34 onlie ddl stop
2019-04-04-11:20:34 begin stop mysqld_3308
2019-04-04-11:20:36 begin rm -rf datadir and cp -rp datadir_bak
2019-04-04-11:22:48 start mysqld_3308
2019-04-04-11:22:53 pt-osc begin
2019-04-04-12:19:15 pt-osc stop

online DDL 花费时间为 34 分钟,pt-osc 花费时间为 57 分钟,使用 onlne DDL 时间约为 pt-osc 工具时间的一半。

做 DDL 参考:

实施

由于是一主一从实例,应用是连接的 vip,删除重建索引采用 online DDL 来做。

停止主从复制后,先在从实例上做(不记录 binlog),主从切换,再在新切换的从实例上做(不记录 binlog):

function red_echo () {local what="$*"echo -e "$(date +%F-%T)  ${what}"
}function check_las_comm(){if [ "$1" != "0" ];thenred_echo "$2"echo "exit 1"exit 1fi
}red_echo "stop slave"
mysql -uroot -p$passwd --socket=/datas/mysql/data/${port}/mysqld.sock -e"stop slave"
check_las_comm "$?" "stop slave failed"red_echo "online ddl begin"mysql -uroot -p$passwd --socket=/datas/mysql/data/${port}/mysqld.sock -e"set sql_log_bin=0;select now() as  ddl_start;ALTER TABLE $db_.\`${table_name}\` DROP FOREIGN KEY FK_arrival_record_product,drop index IXFK_arrival_record,add index idx_product_id_sequence_station_no(product_id,sequence,station_no),add index idx_receive_time(receive_time);select now() as ddl_stop" >>${log_file} 2>& 1red_echo "onlie ddl stop"red_echo "add foreign key"mysql -uroot -p$passwd --socket=/datas/mysql/data/${port}/mysqld.sock -e"set sql_log_bin=0;ALTER TABLE $db_.${table_name} ADD CONSTRAINT _FK_${table_name}_product FOREIGN KEY (product_id) REFERENCES cq_new_cimiss.product (id) ON DELETE NO ACTION ON UPDATE NO ACTION;" >>${log_file} 2>& 1check_las_comm "$?" "add foreign key error"red_echo "add foreign key stop"red_echo "start slave"
mysql -uroot -p$passwd --socket=/datas/mysql/data/${port}/mysqld.sock -e"start slave"
check_las_comm "$?" "start slave failed"

执行时间:

2019-04-08-11:17:36 stop slave
mysql: [Warning] Using a password on the command line interface can be insecure.
ddl_start
2019-04-08 11:17:36
ddl_stop
2019-04-08 11:45:13
2019-04-08-11:45:13 onlie ddl stop
2019-04-08-11:45:13 add foreign key
mysql: [Warning] Using a password on the command line interface can be insecure.
2019-04-08-12:33:48 add foreign key stop
2019-04-08-12:33:48 start slave

删除重建索引花费时间为 28 分钟,添加外键约束时间为 48 分钟。

再次查看 delete 和 select 语句的执行计划:

explain select count(*) from arrival_record where receive_time < STR_TO_DATE('2019-03-10', '%Y-%m-%d')\G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: arrival_record
partitions: NULL
type: range
possible_keys: idx_receive_time
key: idx_receive_time
key_len: 6
ref: NULL
rows: 7540948
filtered: 100.00
Extra: Using where; Using indexexplain 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\G;
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: arrival_record
partitions: NULL
type: range
possible_keys: idx_product_id_sequence_station_no,idx_receive_time
key: idx_receive_time
key_len: 6
ref: NULL
rows: 291448
filtered: 16.66
Extra: Using index condition; Using where

都使用到了 idx_receive_time 索引,扫描的行数大大降低。

索引优化后

delete 还是花费了 77s 时间:

delete from arrival_record where receive_time < STR_TO_DATE('2019-03-10', '%Y-%m-%d')\G

delete 语句通过 receive_time 的索引删除 300 多万的记录花费 77s 时间。

delete 大表优化为小批量删除

应用端已优化成每次删除 10 分钟的数据(每次执行时间 1s 左右),xxx 中没在出现 SLA(主从延迟告警):

另一个方法是通过主键的顺序每次删除 20000 条记录:

#得到满足时间条件的最大主键ID
#通过按照主键的顺序去 顺序扫描小批量删除数据
#先执行一次以下语句SELECT MAX(id) INTO @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;select ROW_COUNT();  #返回20000#执行小批量delete后会返回row_count(), 删除的行数
#程序判断返回的row_count()是否为0,不为0执行以下循环,为0退出循环,删除操作完成DELETE FROM arrival_record WHERE id<@need_delete_max_id LIMIT 20000;select ROW_COUNT();
#程序睡眠0.5s

总结

表数据量太大时,除了关注访问该表的响应时间外,还要关注对该表的维护成本(如做 DDL 表更时间太长,delete 历史数据)。

对大表进行 DDL 操作时,要考虑表的实际情况(如对该表的并发表,是否有外键)来选择合适的 DDL 变更方式。

对大数据量表进行 delete,用小批量删除的方式,减少对主实例的压力和主从延迟。

一入职!就遇到MySQL亿级大表优化....相关推荐

  1. MySQL千万级大表优化解决方案

    MySQL千万级大表优化解决方案 参考文章: (1)MySQL千万级大表优化解决方案 (2)https://www.cnblogs.com/yliucnblogs/p/10096530.html 备忘 ...

  2. 来个硬货——长文解读:基于业务场景的MySQL千万级大表优化

    千万级大表如何优化,这是一个很有技术含量的问题,通常我们的直觉思维都会跳转到拆分或者数据分区,在此我想做一些补充和梳理,想和大家做一些这方面的经验总结,也欢迎大家提出建议. 从一开始脑海里开始也是火光 ...

  3. 详记一次MySQL千万级大表优化过程!

    来自:知乎,作者:互联网编程 链接:https://www.zhihu.com/question/19719997/answer/549041957 问题概述 使用阿里云rds for MySQL数据 ...

  4. 分享:详记一次MySQL千万级大表优化过程!

    问题概述 使用阿里云rds for MySQL数据库(就是MySQL5.6版本),有个用户上网记录表6个月的数据量近2000万,保留最近一年的数据量达到4000万,查询速度极慢,日常卡死.严重影响业务 ...

  5. 亿级大表分库分表实战总结(万字干货,实战复盘)

    亿级大表分库分表实战总结(万字干货,实战复盘) 以下文章来源于阿丸笔记 ,作者阿丸笔记 阿丸笔记 分库分表的文章网上非常多,但是大多内容比较零散,以讲解知识点为主,没有完整地说明一个大表的切分.新架构 ...

  6. 如何优化MySQL千万级大表

    很好的一篇博客,转载 如何优化MySQL千万级大表 原文链接::https://blog.csdn.net/yangjianrong1985/article/details/102675334 千万级 ...

  7. 亿级大表在线不锁表变更字段与索引

    摘要:在业界中有一个比较成熟的工具,针对大表的场景,可以在线进行Alter变更,且不会出现锁表的风险.除此之外,它还有其他的一些优点,让我们开始探索吧. 背景 大家在日常工作中,往往需要对数据库的表结 ...

  8. [生产库实战] 如何使用触发器对生产库上亿级大表进行实时同步

    触发器迁移数据和Oracle 物化视图(MV)的原理相同,通过在源表创建Trigger,记录源表的DML操作日志,然后通过存储过程同步DML受影响的记录,达到目标表和源表数据一致的效果.此方法只是对P ...

  9. MariaDB 10.3 instant ADD COLUMN亿级大表毫秒级加字段

    加字段是痛苦的,需要对表进行重建,尤其是对亿级别的大表,虽然Online DDL可以避免锁表,但如果在主库上执行耗时30分钟,那么再复制到从库上执行,主从复制就出现延迟.使用instant ADD C ...

最新文章

  1. NodeJS学习之网络操作
  2. Debain/ArchLinux/Gentoo 等将合并为超级Linux
  3. String,StringBuffer和StringBuilder的区别
  4. Avdshare Audio Converter 7中文版
  5. 105_键盘事件对象
  6. Sina App Engine 介绍
  7. SAP Spartacus名为Configuration的injection token
  8. 搜狗高速浏览器打开网页没有声音怎么办
  9. 怎样通过FineReader 的“文本”窗口检查文本
  10. 软件调试中的断点分类
  11. Struts--result详解
  12. C语言学习笔记---001C语言的介绍,编译过程原理,工具等
  13. redis java eval_【宇润日常疯测-002】Redis-eval() 到底好在哪
  14. c#语法复习总结(1)-浅谈c#.net
  15. 人工智能ai医学辅助系统_不同的人工智能(AI)技术彻底改变了医学领域(AIM)...
  16. X509证书认证原理
  17. ubuntu安装opencv viz模块
  18. csv文档转为tsv文档-csv to tsv
  19. MockMockito使用手册
  20. 全能电子地图实时路况_全能SUV与城市SUV的区别在哪?日产奇骏对比本田皓影

热门文章

  1. Linux中包的管理与程序安装
  2. Windows 10 RedStone2值得期待的五大功能猜想
  3. python matplotlib模块——绘制三维图形、三维数据散点图
  4. Oversea company interview question.
  5. 安卓如何限制横屏和竖屏
  6. 【原创】Android VMP加壳 POC
  7. CSS 文字处理总结
  8. Fedora 17安装NVIDIA显卡驱动
  9. SQL Server中的CLR编程——用.NET为SQL Server编写存储过程和函数
  10. 保护数据安全的三种武器