日常开发中,我们经常会遇到数据库慢查询。那么导致数据慢查询都有哪些常见的原因呢?今天就跟大家聊聊导致 MySQL 慢查询的 12 个常见原因,以及对应的解决方法:
很多时候,我们的慢查询,都是因为没有加索引。如果没有加索引的话,会导致全表扫描的。因此,应考虑在 where 的条件列,建立索引,尽量避免全表扫描
有时候我们明明加了索引了,但是索引却不生效。在哪些场景,索引会不生效呢?主要有以下十大经典场景:
limit深分页为什么会导致 SQL 变慢呢?假设我们有表结构如下:
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),
KEY idx_create_time (create_time) //索引
) ENGINE=InnoDB AUTO_INCREMENT=1570068 DEFAULT CHARSET=utf8 ROW_FORMAT=REDUNDANT COMMENT='账户表';
执行如下 SQL:
select id,name,balance from account where create_time> '2020-09-19' limit 100000,10;
这个SQL的执行流程:
idx_create_time
,过滤 create_time
条件,找到满足条件的主键 id如下图:
limit 深分页,导致 SQL 变慢原因有两个:
offset+ n
行,然后再丢弃掉前 offset
行,返回后 n 行数据。也就是说 limit 100000,10
,就会扫描100010行,而 limit 0,10,只扫描10行limit 100000,10
扫描更多的行数,也意味着回表更多的次数我们可以通过减少回表次数来优化。一般有:标签记录法、延迟关联法
标签记录法:标记一下上次查询到哪一条了,下次再来查的时候,从该条开始往下扫描
假设上一次记录到 100000,则 SQL 可以修改为:
select id,name,balance FROM account where id > 100000 limit 10;
这样的话,后面无论翻多少页,性能都会不错的,因为命中了 id 主键索引。但是这种方式有局限性:需要一种类似连续自增的字段
延迟关联法:把条件转移到主键索引树,然后减少回表
select acct1.id,acct1.name,acct1.balance FROM account acct1 INNER JOIN (SELECT a.id FROM account a WHERE a.create_time > '2020-09-19' limit 100000, 10) AS acct2 on acct1.id= acct2.id;
优化思路:先通过 idx_create_time
二级索引树查询到满足条件的主键 ID,再与原表通过主键 ID 内连接,这样后面直接走了主键索引了,同时也减少了回表
一个表的数据量达到好几千万或者上亿时,加索引的效果没那么明显啦。性能之所以会变差,是因为维护索引的 B+
树结构层级变得更高了,查询一条数据时,需要经历的磁盘IO变多,因此查询性能变慢
【MYSQL】MYSQL 的学习教程(五)之 MySQL 索引底层:B+ 树详解
一棵高度为 3 的 B+ 树,能存放 1170 *1170 *16 =21902400
,也就是说,可以存放两千万左右的记录。B+ 树高度一般为 1-3 层,已经满足千万级别的数据存储
如果 B+ 树想存储更多的数据,那树结构层级就会更高,查询一条数据时,需要经历的磁盘IO变多,因此查询性能变慢
一般超过千万级别,我们可以考虑分库分表了
分库分表可能导致的问题:
在评估是否分库分表前,先考虑下是否可以把部分历史数据归档,如果可以的话,先不要急着分库分表。如果真的要分库分表,综合考虑和评估方案。比如可以考虑垂直、水平分库分表。水平分库分表策略的话,range范围、hash取模、range+hash取模混合等等
一般来说,不建议使用子查询,可以把子查询改成 join 来优化
而数据库有个规范约定就是:尽量不要有超过3个以上的表连接
MySQL中,join的执行算法,分别是:Index Nested-Loop Join
、Block Nested-Loop Join
Index Nested-Loop Join
:这个 join 算法,跟我们写程序时的嵌套查询类似,并且可以用上被驱动表的索引Block Nested-Loop Join
:被驱动表上没有可用的索引,它会先把驱动表的数据读入线程内存join_buffer 中,再扫描被驱动表,把被驱动表的每一行取出来,跟 join_buffer 中的数据做对比,满足join条件的,作为结果集的一部分返回join 过多的问题:
一般情况下,如果业务需要的话,关联 2~3 个表是可以接受的,但是关联的字段需要加索引哈。如果需要关联更多的表,建议从代码层面进行拆分,在业务层先查询一张表的数据,然后以关联字段作为条件查询关联表形成 map,然后在业务层进行数据的拼装
如果使用了 in,即使后面的条件加了索引,还是要注意in后面的元素不要过多哈。in 元素一般建议不要超过500个,如果超过了,建议分组,每次500一组进行哈
如果我们对in的条件不做任何限制的话,该查询语句一次性可能会查询出非常多的数据,很容易导致接口超时。尤其有时候,我们是用的子查询,in后面的子查询,你都不知道数量有多少那种,更容易采坑。如下这种子查询:
select * from user where user_id in (select author_id from artilce where type = 1);
正例是,分批进行,每批500个:
select user_id,name from user where user_id in (1,2,3...500);
当内存数据页跟磁盘数据页内容不一致的时候,我们称这个内存页为**“脏页”。内存数据写入到磁盘后,内存和磁盘上的数据页的内容就一致了,称为“干净页”**。一般有更新 SQL 才可能会导致脏页
以下的这个更新SQL,如何执行的呢?
update t set c=c+1 where id=666;
如下图:
InnoDB 在处理更新语句的时候,只做了写日志这一个磁盘操作。这个日志叫作 redo log(重做日志)。平时更新 SQL 执行得很快,其实是因为它只是在写内存和 redo log 日志,等到空闲的时候,才把 redo log 日志里的数据同步到磁盘中
redo log 日志不是在磁盘嘛?那为什么不慢?其实是因为写 redo log 的过程是顺序写磁盘的。磁盘顺序写会减少寻道等待时间,速度比随机写要快很多的。
更新 SQL 只是在写内存和 redo log 日志,等到空闲的时候,才把 redo log 日志里的数据同步到磁盘中。这时内存数据页跟磁盘数据页内容不一致,就出现脏页
InnoDB 存储引擎的 redo log 大小是固定,且是环型写入的,如下图:
那什么时候会刷脏页?有几种场景:
InnoDB 用缓冲池(buffer pool)管理内存,而当要读入的数据页没有在内存的时候,就必须到缓冲池中申请一个数据页。这时候只能把最久不使用的数据页从内存中淘汰掉:如果要淘汰的是一个干净页,就直接释放出来复用;但如果是脏页呢,就必须将脏页先刷到磁盘,变成干净页后才能复用
order by 就一定会导致慢查询吗?不是这样的哈,因为 order by 平时用得多,并且数据量一上来,还是走文件排序的话,很容易有慢SQL的
平时经常需要用到 order by ,主要就是用来给某些字段排序的。比如以下 SQL:
select name,age,city from staff where city = '深圳' order by age limit 10;
查看 explain 执行计划的时候,可以看到 Extra 这一列,有一个 Using filesort,它表示用到文件排序
order by 用到文件排序时,为什么查询效率会相对低呢?
order by 排序,分为全字段排序、rowid排序。它是拿 max_length_for_sort_data
和结果行数据长度对比,如果结果行数据长度超过 max_length_for_sort_data
这个值,就会走 rowid 排序,相反,则走全字段排序
rowid 排序,一般需要回表去找满足条件的数据,所以效率会慢一点。以下这个 SQL,使用 rowid 排序,执行过程是这样:
select name,age,city from staff where city = '深圳' order by age limit 10;
sort_buffer
,放入需要排序的 age 字段,以及主键 id主键 id索引树
拿到 id=9 的这一行数据, 取 age 和主键 id 的值,存到 sort_buffer索引树 idx_city
拿到下一个记录的主键 id,即图中的 id=13同样的 SQL,如果是走全字段排序是这样的:
索引树 idx_city
, 找到第一个满足 city='深圳’ 条件的主键 id,也就是图中的 id=9主键id索引树
拿到 id=9 的这一行数据, 取 name、age、city 三个字段的值,存到 sort_buffer索引树idx_city
拿到下一个记录的主键 id,即图中的 id=13sort_buffer
的大小是由一个参数控制的:sort_buffer_size
借助磁盘文件排序的话,效率就更慢一点。因为先把数据放入 sort_buffer,当快要满时。会排一下序,然后把 sort_buffer 中的数据,放到临时磁盘文件,等到所有满足条件数据都查完排完,再用归并算法把磁盘的临时排好序的小文件,合并成一个有序的大文件
order by 使用文件排序,效率会低一点。我们怎么优化呢
max_length_for_sort_data
、sort_buffer_size
等参数优化有时候,我们查询一条很简单的 SQL,但是却等待很长的时间,不见结果返回。一般这种时候就是表被锁住了,或者要查询的某一行或者几行被锁住了。我们只能慢慢等待锁被释放
这时候,我们可以用 show processlist
命令,看看当前语句处于什么状态哈
当 delete 遇到 in 子查询时,即使有索引,也是不走索引的。而对应的 select + in 子查询,却可以走索引
实际执行的时候,MySQL 对 select in 子查询做了优化,把子查询改成 join 的方式,所以可以走索引。但是很遗憾,对于 delete in 子查询,MySQL 却没有对它做这个优化
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;
Using temporary
表示在执行分组的时候使用了临时表Using filesort
表示使用了文件排序group by 是怎么使用到临时表和排序了呢?我们来看下这个SQL的执行流程:
这个流程的执行图如下:
临时表的排序是怎样的呢?
就是把需要排序的字段,放到 sort buffer,排完就返回。在这里注意一点哈,排序分全字段排序和 rowid 排序
因为它既用到临时表,又默认用到排序。有时候还可能用到磁盘临时表
tmp_table_size
),会把内存临时表转成磁盘临时表。从哪些方向去优化呢?
执行 group by 语句为什么需要临时表呢?group by 的语义逻辑,就是统计不同的值出现的个数。如果这个这些值一开始就是有序的,我们是不是直接往下扫描统计就好了,就不用临时表来记录并统计结果啦?
可以有这些优化方案:
SQL_BIG_RESULT
(直接用磁盘临时表。不需要从内存临时表转到磁盘临时表,这个过程很耗时)