转载mysql优化

2022/7/8 2:20:11

本文主要是介绍转载mysql优化,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

https://blog.csdn.net/qq_16504067/article/details/125103336

日常开发中,我们经常会遇到数据库慢查询。那么导致数据慢查询都有哪些常见的原因呢?今天田螺哥就跟大家聊聊导致MySQL慢查询的12个常见原因,以及对应的解决方法。

1. SQL没加索引

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

反例:

select * from user_info where name ='捡田螺的小男孩公众号' ;

正例:

  1.   //添加索引
  2.   alter table user_info add index idx_name (name);

2. SQL 索引不生效

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

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

我们创建一个用户user表

  1.   CREATE TABLE user (
  2.     id int(11) NOT NULL AUTO_INCREMENT,
  3.     userId varchar(32) NOT NULL,
  4.     age  varchar(16) NOT NULL,
  5.     name varchar(255) NOT NULL,
  6.     PRIMARY KEY (id),
  7.     KEY idx_userid (userId) USING BTREE
  8.   ) ENGINE=InnoDB DEFAULT CHARSET=utf8;

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

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

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

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

我们还是用这个表结构:

  1.   CREATE TABLE user (
  2.     id int(11) NOT NULL AUTO_INCREMENT,
  3.     userId varchar(32) NOT NULL,
  4.     age  varchar(16) NOT NULL,
  5.     name varchar(255) NOT NULL,
  6.     PRIMARY KEY (id),
  7.     KEY idx_userid (userId) USING BTREE
  8.   ) 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)三个索引。

假设有以下表结构:

  1.   CREATE TABLE user (
  2.     id int(11) NOT NULL AUTO_INCREMENT,
  3.     user_id varchar(32) NOT NULL,
  4.     age  varchar(16) NOT NULL,
  5.     name varchar(255) NOT NULL,
  6.     PRIMARY KEY (id),
  7.     KEY idx_userid_name (user_id,name) USING BTREE
  8.   ) ENGINE=InnoDB DEFAULT CHARSET=utf8;

有一个联合索引idx_userid_name,我们执行这个SQL,查询条件是name,索引是无效:

explain select * from user where name ='捡田螺的小男孩';

因为查询条件列name不是联合索引idx_userid_name中的第一个列,索引不生效

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

2.5 在索引列上使用mysql的内置函数

表结构:

  1.   CREATE TABLE `user` (
  2.     `id` int(11) NOT NULL AUTO_INCREMENT,
  3.     `userId` varchar(32) NOT NULL,
  4.     `login_time` datetime NOT NULL,
  5.     PRIMARY KEY (`id`),
  6.     KEY `idx_userId` (`userId`) USING BTREE,
  7.     KEY `idx_login_time` (`login_Time`) USING BTREE
  8.   ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;

虽然login_time加了索引,但是因为使用了mysql的内置函数Date_ADD(),索引直接GG,如图:

一般这种情况怎么优化呢?可以把内置函数的逻辑转移到右边,如下:

explain  select * from user where login_time = DATE_ADD('2022-05-22 00:00:00',INTERVAL -1 DAY);

2.6 对索引进行列运算(如,+、-、*、/),索引不生效

表结构:

  1.   CREATE TABLE `user` (
  2.     `id` int(11) NOT NULL AUTO_INCREMENT,
  3.     `userId` varchar(32) NOT NULL,
  4.     `age` int(11) DEFAULT NULL,
  5.     PRIMARY KEY (`id`),
  6.     KEY `idx_age` (`age`) USING BTREE
  7.   ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;

虽然age加了索引,但是因为它进行运算,索引直接迷路了。。。如图:

所以不可以对索引列进行运算,可以在代码处理好,再传参进去。

2.7 索引字段上使用(!= 或者 < >),索引可能失效

表结构:

  1.   CREATE TABLE `user` (
  2.     `id` int(11) NOT NULL AUTO_INCREMENT,
  3.     `userId` int(11) NOT NULL,
  4.     `age` int(11) DEFAULT NULL,
  5.     `name` varchar(255) NOT NULL,
  6.     PRIMARY KEY (`id`),
  7.     KEY `idx_age` (`age`) USING BTREE
  8.   ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;

虽然age加了索引,但是使用了!= 或者< >,not in这些时,索引如同虚设。如下:

其实这个也是跟mySQL优化器有关,如果优化器觉得即使走了索引,还是需要扫描很多很多行的哈,它觉得不划算,不如直接不走索引。平时我们用!= 或者< >,not in的时候,留点心眼哈。

2.8 索引字段上使用is null, is not null,索引可能失效

表结构:

  1.   CREATE TABLE `user` (
  2.     `id` int(11) NOT NULL AUTO_INCREMENT,
  3.     `card` varchar(255) DEFAULT NULL,
  4.     `name` varchar(255) DEFAULT NULL,
  5.     PRIMARY KEY (`id`),
  6.     KEY `idx_name` (`name`) USING BTREE,
  7.     KEY `idx_card` (`card`) USING BTREE
  8.   ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;

单个name字段加上索引,并查询name为非空的语句,其实会走索引的,如下:

单个card字段加上索引,并查询name为非空的语句,其实会走索引的,如下:

但是它两用or连接起来,索引就失效了,如下:

很多时候,也是因为数据量问题,导致了MySQL优化器放弃走索引。同时,平时我们用explain分析SQL的时候,如果type=range,要注意一下哈,因为这个可能因为数据量问题,导致索引无效。

2.9 左右连接,关联的字段编码格式不一样

新建两个表,一个user,一个user_job

  1.   CREATE TABLE `user` (
  2.     `id` int(11) NOT NULL AUTO_INCREMENT,
  3.     `name` varchar(255) CHARACTER SET utf8mb4 DEFAULT NULL,
  4.     `age` int(11) NOT NULL,
  5.     PRIMARY KEY (`id`),
  6.     KEY `idx_name` (`name`) USING BTREE
  7.   ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
  8.    
  9.   CREATE TABLE `user_job` (
  10.     `id` int(11) NOT NULL,
  11.     `userId` int(11) NOT NULL,
  12.     `job` varchar(255) DEFAULT NULL,
  13.     `name` varchar(255) DEFAULT NULL,
  14.     PRIMARY KEY (`id`),
  15.     KEY `idx_name` (`name`) USING BTREE
  16.   ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
  17.    

user表的name字段编码是utf8mb4,而user_job表的name字段编码为utf8

执行左外连接查询,user_job表还是走全表扫描,如下:

如果把它们的name字段改为编码一致,相同的SQL,还是会走索引。

所以大家在做表关联时,注意一下关联字段的编码问题哈。

2.10 优化器选错了索引

MySQL 中一张表是可以支持多个索引的。你写SQL语句的时候,没有主动指定使用哪个索引的话,用哪个索引是由MySQL来确定的。

我们日常开发中,不断地删除历史数据和新增数据的场景,有可能会导致MySQL选错索引。那么有哪些解决方案呢?

  • 使用force index 强行选择某个索引

  • 修改你的SQl,引导它使用我们期望的索引

  • 优化你的业务逻辑

  • 优化你的索引,新建一个更合适的索引,或者删除误用的索引。

3. limit深分页问题

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

3.1 limit深分页为什么会变慢

limit深分页为什么会导致SQL变慢呢?假设我们有表结构如下:

  1.   CREATE TABLE account (
  2.     id int(11) NOT NULL AUTO_INCREMENT COMMENT '主键Id',
  3.     name varchar(255) DEFAULT NULL COMMENT '账户名',
  4.     balance int(11) DEFAULT NULL COMMENT '余额',
  5.     create_time datetime NOT NULL COMMENT '创建时间',
  6.     update_time datetime NOT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  7.     PRIMARY KEY (id),
  8.     KEY idx_name (name),
  9.     KEY idx_create_time (create_time) //索引
  10.   ) 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的执行流程:

  1. 通过普通二级索引树idx_create_time,过滤create_time条件,找到满足条件的主键id

  2. 通过主键id,回到id主键索引树,找到满足记录的行,然后取出需要展示的列(回表过程)

  3. 扫描满足条件的100010行,然后扔掉前100000行,返回。

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

  • limit语句会先扫描offset+n行,然后再丢弃掉前offset行,返回后n行数据。也就是说limit 100000,10,就会扫描100010行,而limit 0,10,只扫描10行。

  • limit 100000,10 扫描更多的行数,也意味着回表更多的次数。

3.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内连接,这样后面直接走了主键索引了,同时也减少了回表。

4. 单表数据量太大

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

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

4.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变多,因此查询性能变慢。

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

一般超过千万级别,我们可以考虑分库分表了。

分库分表可能导致的问题:

  • 事务问题

  • 跨库问题

  • 排序问题

  • 分页问题

  • 分布式ID

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

5. join 或者子查询过多

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

MySQL中,join的执行算法,分别是:Index Nested-Loop JoinBlock 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,然后在业务层进行数据的拼装。

6. 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太多,还可以做个参数校验什么的

  1.   if (userIds.size() > 500) {
  2.       throw new Exception("单次查询的用户Id不能超过200");
  3.   }

7. 数据库在刷脏页

7.1 什么是脏页

当内存数据页跟磁盘数据页内容不一致的时候,我们称这个内存页为“脏页”。内存数据写入到磁盘后,内存和磁盘上的数据页的内容就一致了,称为“干净页”。一般有更新SQL才可能会导致脏页,我们回忆一下:一条更新语句是如何执行的

7.2 一条更新语句是如何执行的?

以下的这个更新SQL,如何执行的呢?

update t set c=c+1 where id=666;
  1. 对于这条更新SQL,执行器会先找引擎取id=666这一行。如果这行所在的数据页本来就在内存中的话,就直接返回给执行器。如果不在内存,就去磁盘读入内存,再返回。

  2. 执行器拿到引擎给的行数据后,给这一行C的值加一,得到新的一行数据,再调用引擎接口写入这行新数据。

  3. 引擎将这行新数据更新到内存中,同时将这个更新操作记录到redo log里面,但是此时redo log 是处于prepare状态的哈。

  4. 执行器生成这个操作的binlog,并把binlog写入磁盘。

  5. 执行器调用引擎的提交事务接口,引擎把刚刚写入的redo log改成提交(commit)状态,更新完成。

InnoDB 在处理更新语句的时候,只做了写日志这一个磁盘操作。这个日志叫作redo log(重做日志)。平时更新SQL执行得很快,其实是因为它只是在写内存和redo log日志,等到空闲的时候,才把redo log日志里的数据同步到磁盘中。

有些小伙伴可能有疑惑,redo log日志不是在磁盘嘛?那为什么不慢?其实是因为写redo log的过程是顺序写磁盘的。磁盘顺序写会减少寻道等待时间,速度比随机写要快很多的。

7.3 为什么会出现脏页呢?

更新SQL只是在写内存和redo log日志,等到空闲的时候,才把redo log日志里的数据同步到磁盘中。这时内存数据页跟磁盘数据页内容不一致,就出现脏页。

7.4 什么时候会刷脏页(flush)?

InnoDB存储引擎的redo log大小是固定,且是环型写入的,如下图(图片来源于MySQL 实战 45 讲):

那什么时候会刷脏页?有几种场景:

  1. redo log写满了,要刷脏页。这种情况要尽量避免的。因为出现这种情况时,整个系统就不能再接受更新啦,即所有的更新都必须堵住。

  2. 内存不够了,需要新的内存页,就要淘汰一些数据页,这时候会刷脏页

InnoDB 用缓冲池(buffer pool)管理内存,而当要读入的数据页没有在内存的时候,就必须到缓冲池中申请一个数据页。这时候只能把最久不使用的数据页从内存中淘汰掉:如果要淘汰的是一个干净页,就直接释放出来复用;但如果是脏页呢,就必须将脏页先刷到磁盘,变成干净页后才能复用。

  1. MySQL 认为系统空闲的时候,也会刷一些脏页

  2. MySQL 正常关闭时,会把内存的脏页都 flush 到磁盘上

7.5 为什么刷脏页会导致SQL变慢呢?

  1. redo log写满了,要刷脏页,这时候会导致系统所有的更新堵住,写性能都跌为0了,肯定慢呀。一般要杜绝出现这个情况。

  2. 一个查询要淘汰的脏页个数太多,一样会导致查询的响应时间明显变长。

8. order by 文件排序

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

8.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,它表示用到文件排序。

 



这篇关于转载mysql优化的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程