高性能MYSQL需要设计最优的库表结构,建立最好的索引,合理的设计查询。查询优化,索引优化,库表结构优化三辆
大车需要起头并进,才能达到最好的性能。
客户端发送一条查询给服务器
服务器先检查查询缓存,如果命中了缓存,则立刻返回存储在缓存中的结果,否则进入下一阶段。
服务器进行SQL解析,预处理,再由优化器生成对应的执行计划,
mysql根据优化器生成的执行计划,调用存储引擎的API来执行查询。
将结果返回给客户端。
了解查询的生命周期,清楚查询的时间消耗情况对于优化查询有很大的意义
查询性能低下的最基本原因是访问的数据太多,可以按照下面两个步骤分析:
1.是否向数据库请求了不需要的数据
带来的问题:对Mysql服务器增加额外的负担,增加网络开销,消耗应用程序服务器的CPU和内存资源
2.MySQL是否在扫描额外的记录
衡量查询开销的三个指标:
mysql> EXPLAIN SELECT * FROM coupon_remind;
+----+-------------+---------------+------+---------------+------------+---------+---------+---------+-------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+---------------+------+---------------+------------+---------+---------+---------+-------+
| 1 | SIMPLE | coupon_remind | ALL | NULL | NULL | NULL | NULL |623753 | NULL |
+----+-------------+---------------+------+---------------+------------+---------+---------+---------+-------+
| id | 查询类型 |查询对象 |访问类型|可能使用的索引 实际使用的索引 索引长度 索引对应的列 检索行数 额外信息
+----+-------------+---------------+------+---------------+------------+---------+---------+---------+-------+
1 row in set (0.03 sec)
常用的类型有: ALL, index, range, ref, eq_ref, const, system, NULL(从左到右,性能从差到好)
红包过期前两天短信提醒,技术方案上面让数据中心凌晨采集两天后过期的数据回流到红包提醒表内,当天下午4点跑定时任务用LIMIT分页加载数据发送短信提醒。每条处理好的记录都会更新状态,峰值回流的数据大概一天有2w条左右。
目前业务处理花费时间:
以2019年1月7号为例子,共处理12800记录,所有业务处理花费4分58秒。最后一次扫表数据查询耗时不过5ms
表结构:
CREATE TABLE `coupon_remind` (
`id` bigint(20) NOT NULL COMMENT '主键ID',
`customer_register_id` varchar(32) NOT NULL DEFAULT '' COMMENT '会员id',
`mobile` varchar(32) NOT NULL DEFAULT '' COMMENT '手机号码',
`open_id` varchar(32) NOT NULL DEFAULT '' COMMENT '公众号openId',
`coupon_id_list` varchar(512) NOT NULL DEFAULT '' COMMENT '红包id集合用;分割',
`content` varchar(200) NOT NULL DEFAULT '' COMMENT '推送内容',
`status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '状态(1未处理,2处理成功)',
`remind_type` tinyint(4) NOT NULL DEFAULT '0' COMMENT '类型(1红包到期提醒)',
`expire_date` bigint(20) NOT NULL DEFAULT '0' COMMENT '过期时间戳',
`curr_date` varchar(32) NOT NULL DEFAULT '0' COMMENT '统计日期,某一天,20180605',
`is_valid` tinyint(2) NOT NULL DEFAULT '0' COMMENT '是否有效',
`last_ver` int(11) NOT NULL DEFAULT '0' COMMENT '版本号',
`create_time` bigint(20) NOT NULL DEFAULT '0' COMMENT '创建时间毫秒',
`op_time` bigint(20) NOT NULL DEFAULT '0' COMMENT '修改时间毫秒',
PRIMARY KEY (`id`),
KEY `index_customer_register_id` (`customer_register_id`) USING BTREE,
KEY `index_curr_date` (`curr_date`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='红包提醒'
查询SQL:
SELECT
id,
customer_register_id,
mobile,
open_id,
coupon_id_list,
content,
status,
remind_type,
expire_date,
curr_date,
is_valid,
last_ver,
create_time,
op_time
FROM
presell_market.coupon_remind
WHERE
curr_date = '20181113' AND status = 1 AND is_valid = 1 LIMIT 100;
上述业务场景单表处理数据10w以下的时候基本上不会有问题,我们模拟下业务超超超级高峰情况下面,查询是否有影响。
模拟数据:20181113存在624805条status = 1 AND is_valid = 1的记录。
可以看出来,越往后面的查询性能瓶颈越大,耗时越高
我们对比下首次查询和尾数查询的执行计划,看下有什么区别
首次查询
尾数查询
通过上述模拟数据我们可以分析,超过10w级别的数据量,查询的性能已经比较差了,尾数业务数据处理则会越来越慢
分析下原因,为什么最后一次查询跟第一次查询性能相差百倍,结合业务场景,随着一批一批数据处理,status状态被更新成2,到最后一些数据的时候,前置已经处理完的数据还是被MySQL服务器计算,筛选到status=1的数据,然后选取100条返回。因此优化的方式在于如何减少查询扫描的数据行数?
我们假设,如果建立curr_date和status的联合索引,可不可以解决这个问题?
完整业务查询(索改)
尾数查询执行计划(索改)
通过跟之前的查询执行过程对比,查询的性能大大提升,但是!!
这个并不是最佳的优化手段,更新操作的耗时变得更长,业务流程中会涉及到status字段更新,索引字段的更新操作,会引起索引重建,存储空间也会相应增大,得不偿失。
我们再对比下两个索引,
SHOW INDEX FROM coupon_remind;
这个命令可以查看这张表创建了哪些索引,命令输出结果列如下:
| Table | Non_unique | Key_name | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment |
首先看下,index on (‘curr_date’)的索引属性:
对比下,index on (‘curr_date’, ‘status’):
两者的Cardinality都很小。
修改索引来优化查询这个方法不可行,那么调整下思路,能不能修改查询语句来减少扫描的数据?
如果我们可以记录上次查询的位置,下一次查询,从这个位置开始,就可以避免扫描计算多余的数据了。所以查询SQL可以调整成这样
SELECT
id,
customer_register_id,
mobile,
open_id,
coupon_id_list,
content,
status,
remind_type,
expire_date,
curr_date,
is_valid,
last_ver,
create_time,
op_time
FROM
presell_market.coupon_remind
WHERE
id > #{beginId} AND curr_date = '20181113' AND status = 1 AND is_valid = 1 LIMIT 100;
跟最初的查询对比,查询性能提升了很多,让我们看下查改之后的第一次执行计划:
改造前
改造后
再看下最后一次查询的执行计划:
改造前
改造后
通过上面两个执行计划可以发现当没有id>xxx这个查询条件的时候,检索的rows要多很多。
修改查询语句之后的完整业务执行流程时间,尾页查询比20w,40w量级的时候耗时更短,通过执行计划可以发现两者的key不同,rows不同,如果我们将语句调整成这样,是否可以让查询变成主键索引,是否可以减少所有查询的RT?
尝试之后发现并没有多少变化
综上所述,通过标记上一次扫描的位置,减少数据的扫描量,可以很好的优化性能,无论是翻到多后面,性能都会很好。
对于索引原理的理解可以帮助我们更好的建立合适的索引。结合实际的业务和执行分析,评估数据的量级,不要想当然的去建立索引。业务上线之后及时去跟进线上慢查询的情况,具体问题具体分析,具体优化。
MySQL官方文档