为什么我的SQL语句使用了索引,但还是会记录到慢查询中?

一、知识科普

问题:mysql 是如何判断一条select语句是否为慢查询呢?
答案:语句的执行时间 > long_query_time

long_query_time 是 mysql 的全局变量,select 语句的执行时间大于该值将被记录在慢查询日志中, long_query_time 的系统默认值是10秒:

mysql> show global variables like 'long_query_time';
+-----------------+-----------+
| Variable_name   | Value     |
+-----------------+-----------+
| long_query_time | 10.000000 |
+-----------------+-----------+
1 row in set (0.02 sec)

当然,在生产上我们不会设置这么大的值,一般会设置为1秒:

mysql> set global long_query_time=1;
Query OK, 0 rows affected (0.00 sec)

设置完后,需要重新连接数据库(因为当前会话不生效)

mysql> show variables like 'long_query_time';
+-----------------+----------+
| Variable_name   | Value    |
+-----------------+----------+
| long_query_time | 1.000000 |
+-----------------+----------+

你也可以根据实际的业务需要,调整到1秒以下,来提升系统响应级别。

二、简单例子:

回答问题前,先看一个简单的例子
第一步:建表:

mysql> CREATE TABLE t (
   -> id int(11) NOT NULL,
   -> a int(11) DEFAULT NULL,
   -> b int(11) DEFAULT NULL,
   -> PRIMARY KEY (id),
   -> KEY a (a)
   -> ) ENGINE=InnoDB;

第二步:再插入2条数据:

mysql> insert into t values(1,1,1),(2,2,2);

第三步:三个语句及其执行计划情况:
语句1:没有使用索引:

mysql> explain select * from t;
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key  | key_len | ref  | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------+
|  1 | SIMPLE      | t     | NULL       | ALL  | NULL          | NULL | NULL    | NULL |    2 |   100.00 | NULL  |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------+

语句2:使用主键索引:

mysql> explain select * from t where id=2;
+----+-------------+-------+------------+-------+---------------+---------+---------+-------+------+----------+-------+
| id | select_type | table | partitions | type  | possible_keys | key     | key_len | ref   | rows | filtered | Extra |
+----+-------------+-------+------------+-------+---------------+---------+---------+-------+------+----------+-------+
|  1 | SIMPLE      | t     | NULL       | const | PRIMARY       | PRIMARY | 4       | const |    1 |   100.00 | NULL  |
+----+-------------+-------+------------+-------+---------------+---------+---------+-------+------+----------+-------+

语句3:使用普通索引:

mysql> explain select a from t;
+----+-------------+-------+------------+-------+---------------+------+---------+------+------+----------+-------------+
| id | select_type | table | partitions | type  | possible_keys | key  | key_len | ref  | rows | filtered | Extra       |
+----+-------------+-------+------------+-------+---------------+------+---------+------+------+----------+-------------+
|  1 | SIMPLE      | t     | NULL       | index | NULL          | a    | 5       | NULL |    2 |   100.00 | Using index |
+----+-------------+-------+------------+-------+---------------+------+---------+------+------+----------+-------------+

三个语句的区别:

  • 语句1:不必多说,没有使用任何索引,数据一多,执行时间就会大于 long_query_time, 被记录在慢查询日志里;
  • 语句2:使用了主键索引,一般情况下速度非常快,不过当cpu压力非常高,超负载运行的时候,执行时间也有可能超过 long_query_time;
  • 语句3:使用了普通索引,但它遍历了整个a索引树,当表数据量很大时,执行时间也会超过 long_query_time;

【简单总结】

  • 是否为慢查询,是由执行时间决定的,而这个执行时间,可能受各种外部因素影响。
  • 是否使用索引与是否为慢查询,没有必然的联系。

三、深入讨论:

3.1 什么叫做 “使用了索引”?

众所周知,InnoDB 是索引组织表,所有的数据都存储在索引树上面的,比如在上例中,表t有一个主键索引,一个普通索引,表数据示意图如下:


表数据示意图

科普:
1.主键索引的叶子节点存的是整行数据。在 InnoDB 里,主键索引也被称为聚簇索引 (clustered index)。
2.非主键索引的叶子节点内容是主键的值。在 InnoDB 里,非主键索引也被称为二级索引 (secondary index)。

如果判断一个语句是否使用索引呢?很容易想到用 explain 结果的 key 是否为空来判断是否使用了索引,这样判断真的对吗?

如何用 explain 结果的key 是否为空来判断是否使用了索引,那么,从逻辑上说,所有在InnoDB表中的查询,都至少用了一个索引(即主键索引)。为什么呢?看下面的例子:比如:select * from t where id > 0; 你觉得它使用了索引吗?

mysql> explain select * from t where id>0;
+----+-------------+-------+------------+-------+---------------+---------+---------+------+------+----------+-------------+
| id | select_type | table | partitions | type  | possible_keys | key     | key_len | ref  | rows | filtered | Extra       |
+----+-------------+-------+------------+-------+---------------+---------+---------+------+------+----------+-------------+
|  1 | SIMPLE      | t     | NULL       | range | PRIMARY       | PRIMARY | 4       | NULL |    2 |   100.00 | Using where |
+----+-------------+-------+------------+-------+---------------+---------+---------+------+------+----------+-------------+

key列显示的是 PRIMARY,即使用了主键索引。为什么会这样呢?因为优化器认为这个语句在执行过程中需要根据主键索引定位到第一个满足id > 0的值,然后再向右遍历索引树,这也算用到了索引。

但我们知道,这个语句一定是做了全表扫描的(因为id是从1开始的),一个全表扫描的语句,你告诉我它还是用了索引,这未免难以让人接受。这也告诉我们,即便 key 列显示不为空,实际上也可能全表扫描的

反过来想,使用这个方式来判读是否使用了索引,那么 InnoDB 只有一种情况叫做没有使用索引,那就是从主键索引的最左边叶子节点开始,向右扫描整个索引树。比如上面例子中的句子1:select * from t;

显然,这种结论是荒谬的,使用这种方式来判断是否使用了索引,也是不科学的

我们再对比语句2(select * from t where id = 2)就能找到答案了,这条语句使用了索引的快速搜索功能,并且有效的减少了扫描的行数,这才是我们平时所说的使用了索引,也是我们判断是否使用索引的有效依据。

3.2 索引的过滤性

举个例子,有一张表 t_person,存了全国14亿人的基本信息,现在想查出年龄在10到15岁之间的小朋友基本信息,你会这么写:

select * from t_people where age between 10 and 15;

同时你会给 age 字段建索引,否则就会全表扫描;但是当你执行之后会发现,这个语句还是非常慢,因为满足这个条件的数据有1亿行;我们看下这条语句的执行过程:


执行过程
  1. 首先在普通索引树上找到第一个满足 age > 10的叶子节点;
    1.1 取出叶子节点的内容id值,去到主键索引树上找到此id的叶子节点,返回整行内容;
  2. 循环向右扫描普通索引树上的下一个叶子节点,重复1.1的动作;
  3. 直到扫描普通索引树上的下一个叶子节点的 age > 15时为止;

我们说一个语句是否使用了索引,其实是关心他的扫描行数。一个大表,不止要有索引,索引的过滤性还要足够好才行,刚才age索引的过滤性就不好。

所谓过滤性好,就是数据的区分度足够高。

索引的过滤性好了,是不是扫描行数就一定少?

下面看一个联合索引的例子:


联合索引示意图

如果你的需求是:找出姓名是张三,年龄是8的所有小朋友:

select * from t_people where name = '张三' and age = 8;

这个所有的过滤性好,而且姓名是张三,年龄是8的所有小朋友应该也不多,因此向右扫描的行数很少,查询效率也很高;
但请不要搞混淆了查询的过滤性和索引的过滤性。比如你现在的需求是:查出姓张且年龄等于8的小朋友:

select * from t_people where name like '张%' and age = 8;

在MySQL5.5及之前版本,这条语句的执行过程如下:


MySQL5.5 联合索引执行过程
  1. 先在联合索引树上,找到第一个字段name是张开头的第一个叶子节点;
    1.1 取出叶子节点的内容id值,去到主键索引树上找到此id的叶子节点(此动作称作回表
    1.1.1 找到的主键索引的叶子中的age值如果等于8,则整行数据放入结果集中,否则进入第二步;
  2. 循环向右扫描联合索引树上的下一个叶子节点,重复1.1、1.2的动作;
  3. 直到扫描联合索引树上的下一个叶子节点的 name不是张开头时为止;

你可以看到,这个执行过程里,最耗时间的步骤就是回表,假设全国姓张的有8000万人,就要回表8000万次。在定位第一行记录的时候,只能使用索引和联合索引的最左前缀 (最左前缀原则)

这种回表次数特别多,性能就不好,有没有优化的办法呢?MySQL5.6 版本给出了优化方案:索引条件下推(index condition pushdown):

MySQL5.6 优化后的执行过程

  1. 先在联合索引树上,找到第一个字段name是张开头,同时判断第二字段age=8的第一个叶子节点;
    1.1 取出叶子节点的内容id值,去到主键索引树上找到此id的叶子节点,返回到结果集中;
  2. 循环向右扫描联合索引树上的下一个叶子节点,重复1.1的动作;
  3. 直到扫描联合索引树上的下一个叶子节点的 name不是张开头时为止;

这个过程和上面的区间是:在遍历整个联合索引树时,将 age = 8 这个条件下推到索引遍历的过程中,减少回表的次数,假设8000万的姓张人中,只有100万个8岁的小朋友,那么整个查询过程中,在联合索引里要遍历8000万次,而回表只要100万次;看见index condtion pushdown的优化效果还是很明显的。但是没能绕开最左前缀原则。

还能不能再优化了?

答案是可以的,我们可以将姓作为单独一列,然后用联合索引 (姓,年龄)。这样只要100完成遍历,100万次回表;
在MySQL5.7中,还引入了虚拟列来实现动态列。

mysql> alter table t_people add name_first varchar(2) generated always as (left(name,1)), add index(name_first,age);
Query OK, 0 rows affected (0.03 sec)

查看一下表结构:

mysql> show create table t_people;
+----------+-----------------------------------------------------------------------------------------------------------+
| Table    | Create Table                                                                                             |
+----------+-----------------------------------------------------------------------------------------------------------+
| t_people | CREATE TABLE `t_people` (
  `id` int NOT NULL,
  `name` varchar(64) DEFAULT NULL,
  `age` int NOT NULL,
  `name_first` varchar(2) GENERATED ALWAYS AS (left(`name`,1)) VIRTUAL,
  PRIMARY KEY (`id`),
  KEY `index_name_age` (`name`,`age`),
  KEY `name_first` (`name_first`,`age`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci |
+----------+-----------------------------------------------------------------------------------------------------------+
1 row in set (0.01 sec)

插入2条记录:

mysql> insert into t_people(id,name,age) values(1,'张三',8),(2,'李四',20);
Query OK, 2 rows affected (0.01 sec)

查看数据:

mysql> select * from t_people;
+----+--------+-----+------------+
| id | name   | age | name_first |
+----+--------+-----+------------+
|  1 | 张三   |   8 | 张         |
|  2 | 李四   |  20 | 李         |
+----+--------+-----+------------+

虚拟列的值不能主动插入,也不能主动修改,它的值在name字段修改的时候自动修改;
有了虚拟列之后,我们的sql就可以这样写了:

mysql> explain select * from t_people where name_first='张' and age=8;
+----+-------------+----------+------------+------+---------------+------------+---------+-------------+------+----------+-------+
| id | select_type | table    | partitions | type | possible_keys | key        | key_len | ref         | rows | filtered | Extra |
+----+-------------+----------+------------+------+---------------+------------+---------+-------------+------+----------+-------+
|  1 | SIMPLE      | t_people | NULL       | ref  | name_first    | name_first | 15      | const,const |    1 |   100.00 | NULL  |
+----+-------------+----------+------------+------+---------------+------------+---------+-------------+------+----------+-------+

完结!
如有帮助,点赞鼓励 👍

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345

推荐阅读更多精彩内容