1、排序优化
2、分组优化
3、分页优化
4、表优化
本篇内容全部围绕SQL语句如何优化,告诉大家实际的优化手段(落实到代码),不绕弯子。
注: 阅读本篇文章需要具备执行计划、索引失效等相关知识。
CREATE TABLE `userinfo` (
`id` int(10) NOT NULL COMMENT '用户id',
`username` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '用户名',
`age` int(3) NULL DEFAULT NULL COMMENT '年龄',
`phone` varchar(15) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '手机号',
`gender` char(1) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '性别: ‘0’-男 ‘1’-女',
`desc` text CHARACTER SET utf8 COLLATE utf8_general_ci NULL COMMENT '自我介绍',
`register_time` datetime(0) NULL DEFAULT NULL COMMENT '注册时间',
`login_time` datetime(0) NULL DEFAULT NULL COMMENT '上一次登录时间',
`pic` varchar(250) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '头像地址',
`look` int(10) NULL DEFAULT NULL COMMENT '查看数',
PRIMARY KEY (`id`)
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
在排序时,尽量使用索引字段进行排序,否则会采用文件排序(filesort),效率低。
show index from userinfo; -- 查询当前表有多少索引
explain select * from userinfo order by username; -- 根据普通字段排序
create index idx_name on userinfo(username); -- 创建索引
explain select * from userinfo order by username; -- filesort
explain select username from userinfo order by username; -- Using index
explain select username,age from userinfo order by username; -- filesort
根据索引字段排序,之后查询的数据必须是索引数上的数据,不可查询额外字段,更不能查询全表字段
其实这一段算是废话来着,谁不知道使用索引能够加快速度啊!还用你说???
好吧,其实这里只想跟你说一个东西:文件排序。
就是上面看到的Using filesort,其实filesort并不代表MySQL使用到了文件进行排序,文件排序只是一种算法,我们习惯把它翻译成"文件排序"而已。一旦看到了filesort,那么就意味着效率很低了。
MySQL在排序时,如果不能够借助索引直接完成排序,那么将会使用文件排序(filesort)。如果使用了filesort,那么MySQL会将数据在内存中进行排序,排序内存由系统变量sort_buffer_size控制。默认为256KB。
注意,排序缓冲区是每个线程是独享的。因此设置太大在并发量高的情况下会消耗MySQL服务器大量内存。
MySQL的排序算法分为两种:
1、如果排序缓冲区大小不足,那么则会采用临时表(temporary table)存储排序结果。之后临时表的行指针信息重新回表查询记录。效率低
2、每个线程都有自己独自的排序缓冲区,如果排序缓冲区设置过大,会浪费内存。
因此sort_buffer_size的大小根据你们需要排序的数据大小来决定。太大浪费内存,太小造成临时表的使用,降低效率。
那MySQL到底采用哪种排序算法?
MySQL4.1版本之前只有多扫描排序算法,单扫描排序是MySQL4.1版本推出的新排序算法,用于优化多扫描排序。MySQL主要根据系统变量max_length_for_sort_data的大小和此次Query语句所取出的所有字段大小之后对比,如果max_length_for_sort_data大,则使用单扫描排序,反之使用多扫描排序。
select @@max_length_for_sort_data;
max_length_for_sort_data默认1KB。
如果是排序场景很多,且每次排序的数据量大。建议使用单排序,即max_length_for_sort_data调的尽可能大,并且保证每次排序的数据都在sort_buffer_size大小之内。
有些网站业务不怎么复杂,专门做统计用的,经常需要升序/降序,并且数据量大。
当然了,上面的场景那些前端UI框架都能帮我们做好,根本不需要发送请求到后台,注意,我说的是类似场景。
在MySQL中group by语句会触发一次默认的order by排序操作,造成不必要的性能浪费。我们可以手动的禁止分组操作带来的排序操作。
CREATE TABLE `student` ( -- 学生表
`id` int(11) NOT NULL,
`name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`age` int(11) NULL DEFAULT NULL,
`c_id` int(11) NULL DEFAULT NULL, -- 班级id
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
CREATE TABLE `class` ( -- 班级表
`id` int(11) NOT NULL,
`name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
INSERT INTO `class` VALUES (1, 'Java01');
INSERT INTO `class` VALUES (2, 'Java02');
INSERT INTO `student` VALUES (1, '小红', 20, 2);
INSERT INTO `student` VALUES (2, '小军', 25, 2);
INSERT INTO `student` VALUES (3, '小明', 24, 1);
INSERT INTO `student` VALUES (4, '小龙', 19, 2);
INSERT INTO `student` VALUES (5, '小蓝', 22, 2);
INSERT INTO `student` VALUES (6, '小刚', 25, 1);
mysql> select avg(age),c_id from student group by c_id;
+----------+------+
| avg(age) | c_id |
+----------+------+
| 24.5000 | 1 |
| 21.5000 | 2 |
+----------+------+
2 rows in set (0.00 sec)
mysql>
发现默认根据c_id排序了。
mysql> select avg(age),c_id from student group by c_id order by null;
+----------+------+
| avg(age) | c_id |
+----------+------+
| 21.5000 | 2 |
| 24.5000 | 1 |
+----------+------+
2 rows in set (0.00 sec)
mysql>
explain select avg(age),c_id from student group by c_id;
explain select avg(age),c_id from student group by c_id order by null;
发现每次的group by 分组操作都会触发一次默认的排序操作,如果我们没有这样的需求,无疑是增加了SQL语句的响应时间
通常使用分页查询是来提高我们的查询效率的,因为通常用户不希望一下子查询到那么多的数据,一般是查询前几条数据,此时的分页效率会比较高。但是有时候在分页查询会遇到一个比较尴尬的问题,那就是limit N,10,即前面跳过N多条记录,只查询N多条记录的后面几条记录。如果N的值非常大,那么效率必然就会很低。
select count(*) from userinfo;
explain select * from userinfo limit 2900000,10; -- 获取sql的执行计划
select * from userinfo limit 2900000,10; -- 执行sql,查看消耗的时间
从执行计划可以看出,进行了全表扫描
花费的时间为:
显然,效率非常低。
我们可以借助索引,在索引上面排序,然后通过索引关联表查询。
explain select * from userinfo u1,(select id from userinfo order by id limit 2900000,10) t where u1.id=t.id;
查看执行消耗时间:
如果id是顺序排列的话,我们可以先根据id进行排序,然后取后面10条。
explain select * from userinfo where id>2900000 limit 10;
当我们对数据库表进行删除(delete)时,这些行只是被标记为“已删除”,而不是真的从索引中物理删除了,因而空间也没有真的被释放回收。而这些被标记的行就是数据空洞。
我们进入/var/lib/mysql/${db_name}目录下查看当前数据库的一些磁盘存储信息。
300W记录大概花了536M的空间。
start transaction; -- 控制在一个事务中(效率高)
delete from userinfo where id > 1000000;
commit;
再次查看磁盘占用空间,发现还是536M。并没有释放空间。
mysql> show table status like 'userinfo'\G;
*************************** 1. row ***************************
Name: userinfo
Engine: InnoDB
Version: 10
Row_format: Compact
Rows: 1110488
Avg_row_length: 492
Data_length: 547356672
Max_data_length: 0
Index_length: 0
Data_free: 99614720
Auto_increment: NULL
Create_time: 2020-06-04 20:28:45
Update_time: 2020-06-04 20:32:59
Check_time: NULL
Collation: utf8_general_ci
Checksum: NULL
Create_options: row_format=COMPACT
Comment:
1 row in set (0.00 sec)
其中Data_free就是碎片(空洞)的数量
当表中存在有大量的数据空洞时,我们可以使用optimize命令来优化表。即删除数据空洞。
InnoDB表优化后会做一个重新构建索引+分析的一个过程。因为我们知道InnoDB引擎将数据与索引是放在一个文件中的,名为.idb,对数据进行整理后,必定会对索引造成影响。而MyISAM是将数据与索引文件分开存储的(.MYD、.MYI),因此MyISAM整理空洞不会对索引造成影响。
optimize table userinfo;
InnoDB表优化后,还会对表中的索引进行重构分析。因此在同等数量的碎片整理,InnoDB花费时间比Myisam表要多一点。
CREATE TABLE `userinfo_myisam` (
`id` int(10) NOT NULL COMMENT '用户id',
`username` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '用户名',
`age` int(3) NULL DEFAULT NULL COMMENT '年龄',
`phone` varchar(15) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '手机号',
`gender` char(1) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '性别: ‘0’-男 ‘1’-女',
`desc` text CHARACTER SET utf8 COLLATE utf8_general_ci NULL COMMENT '自我介绍',
`register_time` datetime(0) NULL DEFAULT NULL COMMENT '注册时间',
`login_time` datetime(0) NULL DEFAULT NULL COMMENT '上一次登录时间',
`pic` varchar(250) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '头像地址',
`look` int(10) NULL DEFAULT NULL COMMENT '查看数',
PRIMARY KEY (`id`)
) ENGINE = MyISAM CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
CREATE PROCEDURE `test_myisam`(count int)
begin
declare i int default 1;
while i<=count do
INSERT INTO userinfo_myisam values(
i,
uuid(),
CEILING(RAND()*90+10),
FLOOR(RAND()*100000000000),
round(FORMAT(rand(),1)),
uuid(),
now(),
now(),
uuid(),
CEILING(RAND()*90+10)
);
set i=i+1;
end while;
end
call test_myisam(3000000); -- 插入300W数据
delete from userinfo_myisam where id>1000000;
optimize table userinfo_myisam;
花费2.65s,比InnoDB效率高,因为InnoDB需要重新构建索引。
再次查看表磁盘占用情况,发现占用空间减少。
总结本篇文章的重要几点:
SQL优化很多方面是索引、锁、参数调优方面的优化,其中锁和索引的优化可以明显的提高SQL语句的查询速度,参数方面的优化则可以帮助我们搭建一个稳定的、高性能、高可用的MySQL集群架构,由于索引、锁、参数调优方面牵扯知识太广,本篇不做讲解。