我熟练应用ctrl c和ctrl v 开发curd代码好多年了。
mysql查询为什么会慢,关于这个问题,在实际开发经常会遇到,而面试中,也是个高频题。
遇到这种问题,我们一般也会想到是因为索引。
那除开索引之外,还有哪些因素会导致数据库查询变慢呢?
有哪些操作,可以提升mysql的查询能力呢?
今天这篇文章,我们就来聊聊会导致数据库查询变慢的场景有哪些,并给出原因和解决方案。
数据库查询流程
我们先来看下,一条查询语句下来,会经历哪些流程。
比如我们有一张数据库表
CREATE TABLE `user` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
`name` varchar(100) NOT NULL DEFAULT '' COMMENT '名字',
`age` int(11) NOT NULL DEFAULT '0' COMMENT '年龄',
`gender` int(8) NOT NULL DEFAULT '0' COMMENT '性别',
PRIMARY KEY (`id`),
KEY `idx_age` (`age`),
KEY `idx_gender` (`gender`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
我们平常写的应用代码(go或C++之类的),这时候就叫客户端了。
客户端底层会带着账号密码,尝试向mysql建立一条TCP长链接。
mysql的连接管理模块会对这条连接进行管理。
建立连接后,客户端执行一条查询sql语句。比如:
select * from user where gender = 1 and age = 100;
客户端会将sql语句通过网络连接给mysql。
mysql收到sql语句后,会在分析器中先判断下SQL语句有没有语法错误,比如select,如果少打一个l,写成slect,则会报错You have an error in your SQL syntax;。这个报错对于我这样的手残党来说可以说是很熟悉了。
接下来是优化器,在这里会根据一定的规则选择该用什么索引。
之后,才是通过执行器去调用存储引擎的接口函数。
Mysql架构
存储引擎类似于一个个组件,它们才是mysql真正获取一行行数据并返回数据的地方,存储引擎是可以替换更改的,既可以用不支持事务的MyISAM,也可以替换成支持事务的Innodb。这个可以在建表的时候指定。比如
CREATE TABLE `user` (
...
) ENGINE=InnoDB;
现在最常用的是InnoDB。
我们就重点说这个。
InnoDB中,因为直接操作磁盘会比较慢,所以加了一层内存提提速,叫buffer pool,这里面,放了很多内存页,每一页16KB,有些内存页放的是数据库表里看到的那种一行行的数据,有些则是放的索引信息。
bufferPool与磁盘
查询SQL到了InnoDB中。会根据前面优化器里计算得到的索引,去查询相应的索引页,如果不在buffer pool里则从磁盘里加载索引页。再通过索引页加速查询,得到数据页的具**置。如果这些数据页不在buffer pool中,则从磁盘里加载进来。
这样我们就得到了我们想要的一行行数据。
索引页与磁盘页的关系
最后将得到的数据结果返回给客户端。
慢查询分析
如果上面的流程比较慢的话,我们可以通过开启profiling看到流程慢在哪。
mysql> set profiling=ON;
Query OK, 0 rows affected, 1 warning (0.00 sec)
mysql> show variables like 'profiling';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| profiling | ON |
+---------------+-------+
1 row in set (0.00 sec)
然后正常执行sql语句。
这些SQL语句的执行时间都会被记录下来,此时你想查看有哪些语句被记录下来了,可以执行 show profiles;
mysql> show profiles;
+----------+------------+---------------------------------------------------+
| Query_ID | Duration | Query |
+----------+------------+---------------------------------------------------+
| 1 | 0.06811025 | select * from user where age>=60 |
| 2 | 0.00151375 | select * from user where gender = 2 and age = 80 |
| 3 | 0.00230425 | select * from user where gender = 2 and age = 60 |
| 4 | 0.00070400 | select * from user where gender = 2 and age = 100 |
| 5 | 0.07797650 | select * from user where age!=60 |
+----------+------------+---------------------------------------------------+
5 rows in set, 1 warning (0.00 sec)
关注下上面的query_id,比如select * from user where age>=60对应的query_id是1,如果你想查看这条SQL语句的具体耗时,那么可以执行以下的命令。
mysql> show profile for query 1;
+----------------------+----------+
| Status | Duration |
+----------------------+----------+
| starting | 0.000074 |
| checking permissions | 0.000010 |
| Opening tables | 0.000034 |
| init | 0.000032 |
| System lock | 0.000027 |
| optimizing | 0.000020 |
| statistics | 0.000058 |
| preparing | 0.000018 |
| executing | 0.000013 |
| Sending data | 0.067701 |
| end | 0.000021 |
| query end | 0.000015 |
| closing tables | 0.000014 |
| freeing items | 0.000047 |
| cleaning up | 0.000027 |
+----------------------+----------+
15 rows in set, 1 warning (0.00 sec)
通过上面的各个项,大家就可以看到具体耗时在哪。比如从上面可以看出Sending data的耗时最大,这个是指执行器开始查询数据并将数据发送给客户端的耗时,因为我的这张表符合条件的数据有好几万条,所以这块耗时最大,也符合预期。
一般情况下,我们开发过程中,耗时大部分时候都在Sending data阶段,而这一阶段里如果慢的话,最容易想到的还是索引相关的原因。
索引相关原因
索引相关的问题,一般能用explain命令帮助分析。通过它能看到用了哪些索引,大概会扫描多少行之类的信息。
mysql会在优化器阶段里看下选择哪个索引,查询速度会更快。
一般主要考虑几个因素,比如:
- 选择这个索引大概要扫描多少行(rows)
- 为了把这些行取出来,需要读多少个16kb的页
- 走普通索引需要回表,主键索引则不需要,回表成本大不大?
回到show profile中提到的sql语句,我们使用explain select * from user where age>=60 分析一下。
explain sql
上面的这条语句,使用的type为ALL,意味着是全表扫描,possible_keys是指可能用得到的索引,这里可能使用到的索引是为age建的普通索引,但实际上数据库使用的索引是在key那一列,是NULL。也就是说这句sql不走索引,全表扫描。
这个是因为数据表里,符合条件的数据行数(rows)太多,如果使用age索引,那么需要将它们从age索引中读出来,并且age索引是普通索引,还需要回表找到对应的主键才能找到对应的数据页。算下来还不如直接走主键划算。于是最终选择了全表扫描。
当然上面只是举了个例子,实际上,mysql执行sql时,不用索引或者用的索引不符合我们预期这件事经常发生,索引失效的场景有很多,比如用了不等号,隐式转换等,这个相信大家背八股文的时候也背过不少了,我也不再赘述。
聊两个生产中容易遇到的问题吧。
索引不符合预期
实际开发中有些情况比较特殊,比如有些数据库表一开始数据量小,索引少,执行sql时,确实使用了符合你预期的索引。但随时时间边长,开发的人变多了,数据量也变大了,甚至还可能会加入一些其他重复多余的索引,就有可能出现用着用着,用到了不符合你预期的其他索引了。从而导致查询突然变慢。
这种问题,也好解决,可以通过force index指定索引。比如
force index指定索引
通过explain可以看出,加了force index之后,sql就选用了idx_age这个索引了。
走了索引还是很慢
有些sql,用explain命令看,明明是走索引的,但还是很慢。一般是两种情况:
第一种是索引区分度太低,比如网页全路径的url链接,这拿来做索引,一眼看过去全都是同一个域名,如果前缀索引的长度建得不够长,那这走索引跟走