MySQL数据库慢查询的原因

本文探讨了大数据表查询性能下降的原因,如单表数据量过大导致索引效果减弱,以及B+树结构的影响。提出了分库分表作为解决方案,并列举了分库分表可能带来的挑战。同时,分析了SQL索引不生效的常见场景,如隐式类型转换、OR条件、LIKE通配符等。此外,还讨论了IN操作符、ORDER BY的文件排序问题,以及LIMIT深分页的优化策略。最后,提到了JOIN和子查询过多、锁等待以及GROUP BY使用临时表对查询性能的影响。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1. 单表数据量太大

1.1 单表数据量太大为什么会变慢?

一个表的数据量达到好几千万或者上亿时,加索引的效果没那么明显啦。性能之所以会变差,是因为维护索引的B+树结构层级变得更高了,查询一条数据时,需要经历的磁盘IO变多,因此查询性能变慢。

1.2 一棵B+树可以存多少数据量

大家是否还记得,一个B+树大概可以存放多少数据量呢?
InnoDB存储引擎最小储存单元是页,一页大小就是16k。
B+树叶子存的是数据,内部节点存的是键值+指针。索引组织表通过非叶子节点的二分查找法以及指针确定数据在哪个页中,进而再去数据页中找到需要的数据;
在这里插入图片描述

假设B+树的高度为2的话,即有一个根结点和若干个叶子结点。这棵B+树的存放总记录数为=根结点指针数*单个叶子节点记录行数。

  • 如果一行记录的数据大小为1k,那么单个叶子节点可以存的记录数 =16k/1k =16.
  • 非叶子节点内存放多少指针呢?我们假设主键ID为bigint类型,长度为8字节(面试官问你int类型,一个int就是32位,4字节),而指针大小在InnoDB源码中设置为6字节,所以就是8+6=14字节,16k/14B =16*1024B/14B = 1170

因此,一棵高度为2的B+树,能存放1170 * 16=18720条这样的数据记录。同理一棵高度为3的B+树,能存放1170 *1170 *16 =21902400,也就是说,可以存放两千万左右的记录。B+树高度一般为1-3层,已经满足千万级别的数据存储。
如果B+树想存储更多的数据,那树结构层级就会更高,查询一条数据时,需要经历的磁盘IO变多,因此查询性能变慢。

1.3 如何解决单表数据量太大,查询变慢的问题

一般超过千万级别,我们可以考虑分库分表了。
分库分表可能导致的问题:

  • 事务问题
  • 跨库问题
  • 排序问题
  • 分页问题
  • 分布式ID

因此,大家在评估是否分库分表前,先考虑下,是否可以把部分历史数据归档先,如果可以的话,先不要急着分库分表。如果真的要分库分表,综合考虑和评估方案。比如可以考虑垂直、水平分库分表。水平分库分表策略的话,range范围、hash取模、range+hash取模混合等等。

2. SQL 索引不生效

有时候我们明明加了索引了,但是索引却不生效。在哪些场景,索引会不失效呢?主要有以下十大经典场景:

2.1 隐式的类型转换,索引失效

我们创建一个用户user表

CREATE TABLE user (
  id int(11) NOT NULL AUTO_INCREMENT,
  userId varchar(32) NOT NULL,
  age  varchar(16) NOT NULL,
  name varchar(255) NOT NULL,
  PRIMARY KEY (id),
  KEY idx_userid (userId) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

userId字段为字串类型,是B+树的普通索引,如果查询条件传了一个数字过去,会导致索引失效。如下:

如果给数字加上’',也就是说,传的是一个字符串呢,当然是走索引,如下图:

为什么第一条语句未加单引号就不走索引了呢?这是因为不加单引号时,是字符串跟数字的比较,它们类型不匹配,MySQL会做隐式的类型转换,把它们转换为浮点数再做比较。隐式的类型转换,索引会失效。

2.2 查询条件包含or,可能导致索引失效

我们还是用这个表结构:

CREATE TABLE user (
  id int(11) NOT NULL AUTO_INCREMENT,
  userId varchar(32) NOT NULL,
  age  varchar(16) NOT NULL,
  name varchar(255) NOT NULL,
  PRIMARY KEY (id),
  KEY idx_userid (userId) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

其中userId加了索引,但是age没有加索引的。我们使用了or,以下SQL是不走索引的,如下:

对于or+没有索引的age这种情况,假设它走了userId的索引,但是走到age查询条件时,它还得全表扫描,也就是需要三步过程:全表扫描+索引扫描+合并。如果它一开始就走全表扫描,直接一遍扫描就完事。Mysql优化器出于效率与成本考虑,遇到or条件,让索引失效,看起来也合情合理嘛。
注意:如果or条件的列都加了索引,索引可能会走也可能不走,大家可以自己试一试哈。但是平时大家使用的时候,还是要注意一下这个or,学会用explain分析。遇到不走索引的时候,考虑拆开两条SQL。
2.3. like通配符可能导致索引失效。
并不是用了like通配符,索引一定会失效,而是like查询是以%开头,才会导致索引失效。
like查询以%开头,索引失效

explain select * from user where userId like ‘%123’;

把%放后面,发现索引还是正常走的,如下:

explain select * from user where userId like ‘123%’;

既然like查询以%开头,会导致索引失效。我们如何优化呢?

使用覆盖索引
把%放后面

2.4 查询条件不满足联合索引的最左匹配原则

MySQl建立联合索引时,会遵循最左前缀匹配的原则,即最左优先。如果你建立一个(a,b,c)的联合索引,相当于建立了(a)、(a,b)、(a,b,c)三个索引。
假设有以下表结构:

CREATE TABLE user (
  id int(11) NOT NULL AUTO_INCREMENT,
  user_id varchar(32) NOT NULL,
  age  varchar(16) NOT NULL,
  name varchar(255) NOT NULL,
  PRIMARY KEY (id),
  KEY idx_userid_name (user_id,name) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

有一个联合索引idx_userid_name,我们执行这个SQL,查询条件是name,索引是无效:
explain select * from user where name =‘捡田螺的小男孩’;
复制代码
因为查询条件列name不是联合索引idx_userid_name中的第一个列,索引不生效

在联合索引中,查询条件满足最左匹配原则时,索引才正常生效。

3. in元素过多

如果使用了in,即使后面的条件加了索引,还是要注意in后面的元素不要过多哈。in元素一般建议不要超过500个,如果超过了,建议分组,每次500一组进行哈。
反例:

select user_id,name from user where user_id in (1,2,3...1000000); 

如果我们对in的条件不做任何限制的话,该查询语句一次性可能会查询出非常多的数据,很容易导致接口超时。尤其有时候,我们是用的子查询,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);

如果传参的ids太多,还可以做个参数校验什么的
if (userIds.size() > 500) {
throw new Exception(“单次查询的用户Id不能超过200”);
}

4. order by 文件排序

order by就一定会导致慢查询吗?不是这样的哈,因为order by平时用得多,并且数据量一上来,还是走文件排序的话,很容易有慢SQL的。听我娓娓道来,order by哪些时候可能会导致慢SQL哈。

4.1 order by 的 Using filesort文件排序

我们平时经常需要用到order by ,主要就是用来给某些字段排序的。比如以下SQL:

select name,age,city from staff where city = '深圳' order by age limit 10;

它表示的意思就是:查询前10个,来自深圳员工的姓名、年龄、城市,并且按照年龄小到大排序。

查看explain执行计划的时候,可以看到Extra这一列,有一个Using filesort,它表示用到文件排序。

4.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中的数据,放到临时磁盘文件,等到所有满足条件数据都查完排完,再用归并算法把磁盘的临时排好序的小文件,合并成一个有序的大文件。

4.3 如何优化order by的文件排序

order by使用文件排序,效率会低一点。我们怎么优化呢?

因为数据是无序的,所以就需要排序。如果数据本身是有序的,那就不会再用到文件排序啦。而索引数据本身是有序的,我们通过建立索引来优化order by语句。
我们还可以通过调整max_length_for_sort_data、sort_buffer_size等参数优化;

5 . 系统硬件或网络资源

如果数据库服务器内存、硬件资源,或者网络资源配置不是很好,就会慢一些哈。这时候可以升级硬件配置。这就好比你的计算机有时候很卡,你可以加个内存条什么的一个道理。
如果数据库压力本身很大,比如高并发场景下,大量请求到数据库来,数据库服务器CPU占用很高或者IO利用率很高,这种情况下所有语句的执行都有可能变慢的哈。这时候你开始排查是不是出什么问题啦。

6. SQL没加索引

很多时候,我们的慢查询,都是因为没有加索引。如果没有加索引的话,会导致全表扫描的。因此,应考虑在where的条件列,建立索引,尽量避免全表扫描。
反例:

select * from user_info where name ='MOnster' ;

正例:
//添加索引

alter table user_info add index idx_name (name);

7. limit深分页问题

limit深分页问题,会导致慢查询,应该大家都司空见惯了吧。

7.1 limit深分页为什么会变慢

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。
通过主键id,回到id主键索引树,找到满足记录的行,然后取出需要展示的列(回表过程)
扫描满足条件的100010行,然后扔掉前100000行,返回。

limit深分页,导致SQL变慢原因有两个:

limit语句会先扫描offset+n行,然后再丢弃掉前offset行,返回后n行数据。也就是说limit 100000,10,就会扫描100010行,而limit 0,10,只扫描10行。
limit 100000,10 扫描更多的行数,也意味着回表更多的次数。

7.2 如何优化深分页问题

我们可以通过减少回表次数来优化。一般有标签记录法和延迟关联法。
标签记录法

就是标记一下上次查询到哪一条了,下次再来查的时候,从该条开始往下扫描。就好像看书一样,上次看到哪里了,你就折叠一下或者夹个书签,下次来看的时候,直接就翻到啦。

假设上一次记录到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内连接,这样后面直接走了主键索引了,同时也减少了回表。

8. join 或者子查询过多

一般来说,不建议使用子查询,可以把子查询改成join来优化。而数据库有个规范约定就是:尽量不要有超过3个以上的表连接。为什么要这么建议呢? 我们来聊聊,join哪些方面可能导致慢查询吧。
MySQL中,join的执行算法,分别是:Index Nested-Loop Join和Block Nested-Loop Join。

Index Nested-Loop Join:这个join算法,跟我们写程序时的嵌套查询类似,并且可以用上被驱动表的索引。
Block Nested-Loop Join:这种join算法,被驱动表上没有可用的索引,它会先把驱动表的数据读入线程内存join_buffer中,再扫描被驱动表,把被驱动表的每一行取出来,跟join_buffer中的数据做对比,满足join条件的,作为结果集的一部分返回。

join过多的问题:

一方面,过多的表连接,会大大增加SQL复杂度。另外一方面,如果可以使用被驱动表的索引那还好,并且使用小表来做驱动表,查询效率更佳。如果被驱动表没有可用的索引,join是在join_buffer内存做的,如果匹配的数据量比较小或者join_buffer设置的比较大,速度也不会太慢。但是,如果join的数据量比较大时,mysql会采用在硬盘上创建临时表的方式进行多张表的关联匹配,这种显然效率就极低,本来磁盘的
IO 就不快,还要关联。

一般情况下,如果业务需要的话,关联2~3个表是可以接受的,但是关联的字段需要加索引哈。如果需要关联更多的表,建议从代码层面进行拆分,在业务层先查询一张表的数据,然后以关联字段作为条件查询关联表形成map,然后在业务层进行数据的拼装。

9. 拿不到锁

有时候,我们查询一条很简单的SQL,但是却等待很长的时间,不见结果返回。一般这种时候就是表被锁住了,或者要查询的某一行或者几行被锁住了。我们只能慢慢等待锁被释放。

举一个生活的例子哈,你和别人合租了一间房子,这个房子只有一个卫生间的话。假设某一时刻,你们都想去卫生间,但是对方比你早了一点点。那么此时你只能等对方出来后才能进去。

这时候,我们可以用show processlist命令,看看当前语句处于什么状态哈。

10. group by使用临时表

group by一般用于分组统计,它表达的逻辑就是根据一定的规则,进行分组。日常开发中,我们使用得比较频繁。如果不注意,很容易产生慢SQL。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Monster_起飞

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值