8.2 order by文件排序效率为什么较低
order by
用到文件排序时,为什么查询效率会相对低呢?
order by
排序,分为全字段排序和rowid排序。它是拿max_length_for_sort_data
和结果行数据长度对比,如果结果行数据长度超过max_length_for_sort_data
这个值,就会走rowid排序,相反,则走全字段排序。
rowid排序
rowid排序,一般需要回表去找满足条件的数据,所以效率会慢一点。以下这个SQL,使用rowid
排序,执行过程是这样:
select name,age,city from staff where city = '深圳' order by age limit 10;
- MySQL 为对应的线程初始化
sort_buffer
,放入需要排序的age
字段,以及主键id
; - 从索引树
idx_city
, 找到第一个满足city='深圳’
条件的主键id
,也就是图中的id=9
; - 到
主键id索引树
拿到id=9
的这一行数据, 取age和主键id
的值,存到sort_buffer
; - 从索引树
idx_city
拿到下一个记录的主键id
,即图中的id=13
; - 重复步骤 3、4 直到
city
的值不等于深圳为止; - 前面5步已经查找到了所有
city
为深圳的数据,在sort_buffer
中,将所有数据根据age进行排序; - 遍历排序结果,取前10行,并按照
id
的值回到原表中,取出city、name 和 age
三个字段返回给客户端。
全字段排序
同样的SQL,如果是走全字段排序是这样的:
select name,age,city from staff where city = '深圳' order by age limit 10;
- MySQL 为对应的线程初始化
sort_buffer
,放入需要查询的name、age、city
字段; - 从索引树
idx_city
, 找到第一个满足city='深圳’
条件的主键 id,也就是图中的id=9
; - 到主键
id索引树
拿到id=9
的这一行数据, 取name、age、city
三个字段的值,存到sort_buffer
; - 从索引树
idx_city
拿到下一个记录的主键id
,即图中的id=13
; - 重复步骤 3、4 直到
city
的值不等于深圳为止; - 前面5步已经查找到了所有
city
为深圳的数据,在sort_buffer
中,将所有数据根据age
进行排序; - 按照排序结果取前10行返回给客户端。
sort_buffer
的大小是由一个参数控制的:sort_buffer_size
。
- 如果要排序的数据小于
sort_buffer_size
,排序在sort_buffer
内存中完成 - 如果要排序的数据大于
sort_buffer_size
,则借助磁盘文件来进行排序。
借助磁盘文件排序的话,效率就更慢一点。因为先把数据放入
sort_buffer
,当快要满时。会排一下序,然后把sort_buffer
中的数据,放到临时磁盘文件,等到所有满足条件数据都查完排完,再用归并算法把磁盘的临时排好序的小文件,合并成一个有序的大文件。
8.3 如何优化order by的文件排序
order by
使用文件排序,效率会低一点。我们怎么优化呢?
- 因为数据是无序的,所以就需要排序。如果数据本身是有序的,那就不会再用到文件排序啦。而索引数据本身是有序的,我们通过建立索引来优化
order by
语句。 - 我们还可以通过调整
max_length_for_sort_data
、sort_buffer_size
等参数优化;
9. 拿不到锁
有时候,我们查询一条很简单的SQL,但是却等待很长的时间,不见结果返回。一般这种时候就是表被锁住了,或者要查询的某一行或者几行被锁住了。我们只能慢慢等待锁被释放。
举一个生活的例子哈,你和别人合租了一间房子,这个房子只有一个卫生间的话。假设某一时刻,你们都想去卫生间,但是对方比你早了一点点。那么此时你只能等对方出来后才能进去。
这时候,我们可以用show processlist
命令,
看看当前语句处于什么状态哈。
10. delete + in子查询不走索引!
之前见到过一个生产慢SQL问题,当delete
遇到in
子查询时,即使有索引,也是不走索引的。而对应的select + in
子查询,却可以走索引。
MySQL版本是5.7,假设当前有两张表account和old_account,表结构如下:
CREATE TABLE `old_account` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键Id',
`name` varchar(255) DEFAULT NULL COMMENT '账户名',
`balance` int(11) DEFAULT NULL COMMENT '余额',
`create_time` datetime NOT NULL COMMENT '创建时间',
`update_time` datetime NOT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
KEY `idx_name` (`name`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1570068 DEFAULT CHARSET=utf8 ROW_FORMAT=REDUNDANT COMMENT='老的账户表';
CREATE TABLE `account` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键Id',
`name` varchar(255) DEFAULT NULL COMMENT '账户名',
`balance` int(11) DEFAULT NULL COMMENT '余额',
`create_time` datetime NOT NULL COMMENT '创建时间',
`update_time` datetime NOT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
KEY `idx_name` (`name`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1570068 DEFAULT CHARSET=utf8 ROW_FORMAT=REDUNDANT COMMENT='账户表';
执行的SQL如下:
delete from account where name in (select name from old_account);
查看执行计划,发现不走索引:
但是如果把delete
换成select
,就会走索引。如下:
为什么select + in
子查询会走索引,delete + in
子查询却不会走索引呢?
我们执行以下SQL看看:
explain select * from account where name in (select name from old_account);
show WARNINGS; //可以查看优化后,最终执行的sql
结果如下:
select `test2`.`account`.`id` AS `id`,`test2`.`account`.`name` AS `name`,`test2`.`account`.`balance` AS `balance`,`test2`.`account`.`create_time` AS `create_time`,`test2`.`account`.`update_time` AS `update_time` from `test2`.`account`
semi join (`test2`.`old_account`)
where (`test2`.`account`.`name` = `test2`.`old_account`.`name`)
可以发现,实际执行的时候,MySQL对select in
子查询做了优化,把子查询改成join
的方式,所以可以走索引。但是很遗憾,对于delete in
子查询,MySQL却没有对它做这个优化。
11、group by使用临时表
group by
一般用于分组统计,它表达的逻辑就是根据一定的规则,进行分组。日常开发中,我们使用得比较频繁。如果不注意,很容易产生慢SQL。
11.1 group by的执行流程
假设有表结构:
CREATE TABLE `staff` (
`id` bigint(11) NOT NULL AUTO_INCREMENT COMMENT '主键id',
`id_card` varchar(20) NOT NULL COMMENT '身份证号码',
`name` varchar(64) NOT NULL COMMENT '姓名',
`age` int(4) NOT NULL COMMENT '年龄',
`city` varchar(64) NOT NULL COMMENT '城市',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=15 DEFAULT CHARSET=utf8 COMMENT='员工表';
我们查看一下这个SQL的执行计划:
explain select city ,count(*) as num from staff group by city;
- Extra 这个字段的
Using temporary
表示在执行分组的时候使用了临时表 - Extra 这个字段的
Using filesort
表示使用了文件排序
group by
是怎么使用到临时表和排序了呢?我们来看下这个SQL的执行流程
select city ,count(*) as num from staff group by city;
- 创建内存临时表,表里有两个字段
city和num
; - 全表扫描
staff
的记录,依次取出city = 'X'
的记录。
- 判断临时表中是否有为
city='X'
的行,没有就插入一个记录(X,1)
; - 如果临时表中有
city='X'
的行,就将X这一行的num值加 1;
- 遍历完成后,再根据字段
city
做排序,得到结果集返回给客户端。这个流程的执行图如下:
临时表的排序是怎样的呢?
就是把需要排序的字段,放到sort buffer,排完就返回。在这里注意一点哈,排序分全字段排序和rowid排序
- 如果是全字段排序,需要查询返回的字段,都放入sort buffer,根据排序字段排完,直接返回
- 如果是rowid排序,只是需要排序的字段放入sort buffer,然后多一次回表操作,再返回。
11.2 group by可能会慢在哪里?
group by
使用不当,很容易就会产生慢SQL 问题。因为它既用到临时表,又默认用到排序。有时候还可能用到磁盘临时表。
- 如果执行过程中,会发现
内存临时表
大小到达了上限(控制这个上限的参数就是tmp_table_size),会把内存临时表转成磁盘临时表。 - 如果数据量很大,很可能这个查询需要的磁盘临时表,就会占用大量的磁盘空间。
11.3 如何优化group by呢?
从哪些方向去优化呢?
- 方向1:既然它默认会排序,我们不给它排是不是就行啦。
- 方向2:既然临时表是影响
group by
性能的X因素,我们是不是可以不用临时表?
我们一起来想下,执行group by
语句为什么需要临时表呢?group by
的语义逻辑,就是统计不同的值出现的个数。如果这个这些值一开始就是有序的,我们是不是直接往下扫描统计就好了,就不用临时表来记录并统计结果啦?
可以有这些优化方案:
- group by 后面的字段加索引
- order by null 不用排序
- 尽量只使用内存临时表
- 使用SQL_BIG_RESULT
12. 系统硬件或网络资源
- 如果数据库服务器内存、硬件资源,或者网络资源配置不是很好,就会慢一些哈。这时候可以升级配置。这就好比你的计算机有时候很卡,你可以加个内存条什么的一个道理。
- 如果数据库压力本身很大,比如高并发场景下,大量请求到数据库来,数据库服务器
CPU
占用很高或者IO利用率
很高,这种情况下所有语句的执行都有可能变慢的哈。
最后
如果测试环境数据库的一些参数配置,和生产环境参数配置不一致的话,也容易产生慢SQL哈。之前见过一个慢SQL的生产案例,就是测试环境用了index merge
,所以查看explain执行计划时,是可以走索引的,但是到了生产,却全表扫描,最后排查发现是生产环境配置把index merge
关闭了。大家是否还遇到其他场景的慢SQL呢?如果有的话,欢迎评论区留言交流哈