吃透MySQL(五):索引的使用详细介绍


通常说的这个查询走索引了是什么意思?

当我们对某个字段的值进行某种检索的时候,如果这个检索过程中,我们能够快速定位到目标数据所在的页,有效的降低页的io操作,而不需要去扫描所有的数据页的时候,我们认为这种情况能够有效的利用索引,也称这个检索可以走索引,如果这个过程中不能够确定数据在那些页中,我们认为这种情况下索引对这个查询是无效的,此查询不走索引。

一,索引简单使用

分为聚集索引非聚集索引,前面已经介绍过,这里不再介绍!

非聚集索引又分为:

  • 单列索引:即一个索引只包含一个列。
  • 多列索引(又称复合索引):即一个索引包含多个列。
  • 唯一索引:索引列的值必须唯一,允许有一个空值。

1,创建索引

# 方式1
create [unique] index 索引名称 on 表名(列名[(length)]);
# 方式2
alter 表名 add [unique] index 索引名称 on (列名[(length)]);

如果字段是char、varchar类型,length可以小于字段实际长度,如果是blog、text等长文本类型,必须指定length。

[unique]:中括号代表可以省略,如果加上了unique,表示创建唯一索引。

如果table后面只写一个字段,就是单列索引,如果写多个字段,就是复合索引,多个字段之间用逗号隔开。

2,删除索引

drop index 索引名称 on 表名;

3,查看索引

show index from 表名;

4,使用示例

4.1,准备200万数据

# 如果test_index数据库存在,则删除
drop database if exists test_index;
# 创建test_index数据库
create database test_index;
# 当前数据库切换到test_index;
use test_index;
# 创建表test1
create table test1(
    -> id int not null comment'编号',
    -> name varchar(20) not null comment'姓名',
    -> sex tinyint not null comment '性别,1:男,2:女',
    -> email varchar(50)
    -> );
# 如果存在存储过程proc1则删除
drop procedure if exists proc1;
# 结束符号改为$
mysql> delimiter $
# 创建存储过程proc1,插入200万数据到test1表中
mysql> create procedure proc1()
    -> begin
    -> declare i int default 1;
    -> start transaction;
    -> while i <= 2000000 do
    ->  insert into test1 (id, name, sex, email) values (i,concat('bobo',i),if(mod(i,2),1,2),concat('bobo',i,'@qq.com'));
    ->  set i = i + 1;
    ->  if i%10000=0 then
    ->   commit;
    ->   start transaction;
    ->  end if;
    -> end while;
    -> commit;
    -> end $
# 结束符号改为 ;
delimiter ;
# 执行存储过程proc1()
call proc1();

上图中使用存储过程循环插入了200万记录,表中有4个字段,除了sex列,其他列的值都是没有重复的,表中还未建索引。

插入的200万数据中,id,name,email的值都是没有重复的。

mysql> delimiter ;
mysql> call proc1();
ERROR 1305 (42000): PROCEDURE test_index.proc1 does not exist
mysql> drop procedure if exists proc1;
Query OK, 0 rows affected, 1 warning (0.03 sec)

mysql> delimiter $
mysql> create procedure proc1()
    -> begin
    -> declare i int default 1;
    -> start transaction;
    -> while i <= 2000000 do
    ->  insert into test1 (id, name, sex, email) values (i,concat('bobo',i),if(mod(i,2),1,2),concat('bobo',i,'@qq.com'));
    ->  set i = i + 1;
    ->  if i%10000=0 then
    ->   commit;
    ->   start transaction;
    ->  end if;
    -> end while;
    -> commit;
    -> end $
Query OK, 0 rows affected (0.10 sec)

mysql> delimiter ;
mysql> call proc1();
Query OK, 0 rows affected (1 min 34.59 sec)

mysql> select count(*) from test1;
+----------+
| count(*) |
+----------+
|  2000000 |
+----------+
1 row in set (0.12 sec)

我们可以看到,插入200万条数据大概用了1分35秒!

4.2,无索引查询

上面我们test1表是没有索引的,下面我们来看一下查询速度:

mysql> select * from test1 a where a.id = 1;
+----+-------+-----+--------------+
| id | name  | sex | email        |
+----+-------+-----+--------------+
|  1 | bobo1 |   1 | bobo1@qq.com |
+----+-------+-----+--------------+
1 row in set (0.90 sec)

上面我们按id查询了一条记录耗时900毫秒,我们在id上面创建个索引感受一下速度。

4.3,创建索引

我们在id上面创建一个索引,感受一下:

mysql> create index idx1 on test1(id);
Query OK, 0 rows affected (28.22 sec)
Records: 0  Duplicates: 0  Warnings: 0

mysql> select * from test1 a where a.id = 1;
+----+-------+-----+--------------+
| id | name  | sex | email        |
+----+-------+-----+--------------+
|  1 | bobo1 |   1 | bobo1@qq.com |
+----+-------+-----+--------------+
1 row in set (0.00 sec)

上面的查询是不是非常快,耗时1毫秒都不到。

4.4,创建索引并指定长度

通过email检索一下数据:

mysql> select * from test1 a where a.email = "[email protected]";
+---------+-------------+-----+--------------------+
| id      | name        | sex | email              |
+---------+-------------+-----+--------------------+
| 1000000 | bobo1000000 |   2 | bobo1000000@qq.com |
+---------+-------------+-----+--------------------+
1 row in set (1.07 sec)

耗时1秒多,回头去看一下插入数据的sql,我们可以看到所有的email记录,每条记录的前面15个字符是不一样的,结尾是一样的(都是@qq.com),通过前面15个字符就可以定位一个email了,那么我们可以对email创建索引的时候指定一个长度为15,这样相对于整个email字段更短一些,查询效果是一样的,这样一个页中可以存储更多的索引记录,命令如下:

mysql> create index idx2 on test1(email(15));
Query OK, 0 rows affected (54.25 sec)
Records: 0  Duplicates: 0  Warnings: 0

然后看一下查询效果:

mysql> select * from test1 a where a.email = "[email protected]";
+---------+-------------+-----+--------------------+
| id      | name        | sex | email              |
+---------+-------------+-----+--------------------+
| 1000000 | bobo1000000 |   2 | bobo1000000@qq.com |
+---------+-------------+-----+--------------------+
1 row in set (0.00 sec)

耗时不到1毫秒,神速。

4.5,查看表中的索引

我们看一下test1表中的所有索引,如下:

mysql> show index from test1;
+-------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+---------+------------+
| Table | Non_unique | Key_name | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment | Visible | Expression |
+-------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+---------+------------+
| test1 |          1 | idx1     |            1 | id          | A         |     1992580 |     NULL |   NULL |      | BTREE      |         |               | YES     | NULL       |
| test1 |          1 | idx2     |            1 | email       | A         |     1992580 |       15 |   NULL | YES  | BTREE      |         |               | YES     | NULL       |
+-------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+---------+------------+
2 rows in set (0.05 sec)

可以看到test1表中2个索引的详细信息(索引名称、类型,字段)。

4.5,删除索引

mysql> drop index idx2 on test1;
Query OK, 0 rows affected (0.16 sec)
Records: 0  Duplicates: 0  Warnings: 0

二,b+树中数据检索过程

1,唯一记录检索

在这里插入图片描述

如上图,所有的数据都是唯一的,查询105的记录,过程如下:

  1. 将P1页加载到内存
  2. 在内存中采用二分法查找,可以确定105位于[100,150)中间,所以我们需要去加载100关联P4页
  3. 将P4加载到内存中,采用二分法找到105的记录后退出。

2,查询某个值的所有记录

在这里插入图片描述

如上图,查询105的所有记录,过程如下:

  1. 将P1页加载到内存
  2. 在内存中采用二分法查找,可以确定105位于[100,150)中间,100关联P4页
  3. 将P4加载到内存中,采用二分法找到最有一个小于105的记录,即100,然后通过链表从100开始向后访问,找到所有的105记录,直到遇到第一个大于100的值为止

3,范围查找

在这里插入图片描述

数据如上图,查询[55,150]所有记录,由于页和页之间是双向链表升序结构,页内部的数据是单项升序链表结构,所以只用找到范围的起始值所在的位置,然后通过依靠链表访问两个位置之间所有的数据即可,过程如下:

  1. 将P1页加载到内存
  2. 内存中采用二分法找到55位于50关联的P3页中,150位于P5页中
  3. 将P3加载到内存中,采用二分法找到第一个55的记录,然后通过链表结构继续向后访问P3中的60、67,当P3访问完毕之后,通过P3的nextpage指针访问下一页P4中所有记录,继续遍历P4中的所有记录,直到访问到P5中所有的150为止。

4,模糊匹配

在这里插入图片描述

数据如上图。

查询以f开头的所有记录

过程如下:

  1. 将P1数据加载到内存中
  2. 在P1页的记录中采用二分法找到最后一个小于等于f的值,这个值是f,以及第一个大于f的,这个值是z,f指向叶节点P3,z指向叶节点P6,此时可以断定以f开头的记录可能存在于[P3,P6)这个范围的页内,即P3、P4、P5这三个页中
  3. 加载P3这个页,在内部以二分法找到第一条f开头的记录,然后以链表方式继续向后访问P4、P5中的记录,即可以找到所有已f开头的数据

查询包含f的记录

包含的查询在sql中的写法是%f%,通过索引我们还可以快速定位所在的页么?

可以看一下上面的数据,f在每个页中都存在,我们通过P1页中的记录是无法判断包含f的记录在那些页的,只能通过io的方式加载所有叶子节点,并且遍历所有记录进行过滤,才可以找到包含f的记录。

所以如果使用了%值%这种方式,索引对查询是无效的。

5,最左匹配原则

当b+树的数据项是复合的数据结构,比如(name,age,sex)的时候,b+树是按照从左到右的顺序来建立搜索树的,比如当(张三,20,F)这样的数据来检索的时候,b+树会优先比较name来确定下一步的所搜方向,如果name相同再依次比较age和sex,最后得到检索的数据;但当(20,F)这样的没有name的数据来的时候,b+树就不知道下一步该查哪个节点,因为建立搜索树的时候name就是第一个比较因子,必须要先根据name来搜索才能知道下一步去哪里查询。比如当(张三,F)这样的数据来检索时,b+树可以用name来指定搜索方向,但下一个字段age的缺失,所以只能把名字等于张三的数据都找到,然后再匹配性别是F的数据了, 这个是非常重要的性质,即索引的最左匹配特性。

来一些示例我们体验一下。

下图中是3个字段(a,b,c)的联合索引,索引中数据的顺序是以a asc,b asc,c asc这种排序方式存储在节点中的,索引先以a字段升序,如果a相同的时候,以b字段升序,b相同的时候,以c字段升序,节点中每个数据认真看一下。

在这里插入图片描述

查询a=1的记录

由于页中的记录是以a asc,b asc,c asc这种排序方式存储的,所以a字段是有序的,可以通过二分法快速检索到,过程如下:

  1. 将P1加载到内存中
  2. 在内存中对P1中的记录采用二分法找,可以确定a=1的记录位于{1,1,1}和{1,5,1}关联的范围内,这两个值子节点分别是P2、P4
  3. 加载叶子节点P2,在P2中采用二分法快速找到第一条a=1的记录,然后通过链表向下一条及下一页开始检索,直到在P4中找到第一个不满足a=1的记录为止

查询a=1 and b=5的记录

方法和上面的一样,可以确定a=1 and b=5的记录位于{1,1,1}和{1,5,1}关联的范围内,查找过程和a=1查找步骤类似。

查询b=1的记录

这种情况通过P1页中的记录,是无法判断b=1的记录在那些页中的,只能加锁索引树所有叶子节点,对所有记录进行遍历,然后进行过滤,此时索引是无效的。

按照c的值查询

这种情况和查询b=1也一样,也只能扫描所有叶子节点,此时索引也无效了。

按照b和c一起查

这种也是无法利用索引的,也只能对所有数据进行扫描,一条条判断了,此时索引无效。

按照[a,c]两个字段查询

这种只能利用到索引中的a字段了,通过a确定索引范围,然后加载a关联的所有记录,再对c的值进行过滤。

查询a=1 and b>=0 and c=1的记录

这种情况只能先确定a=1 and b>=0所在页的范围,然后对这个范围的所有页进行遍历,c字段在这个查询的过程中,是无法确定c的数据在哪些页的,此时我们称c是不走索引的,只有a、b能够有效的确定索引页的范围。

类似这种的还有>、<、between and,多字段索引的情况下,mysql会一直向右匹配直到遇到范围查询(>、<、between、like)就停止匹配。

上面说的各种情况,大家都多看一下图中数据,认真分析一下查询的过程,基本上都可以理解了。

上面这种查询叫做最左匹配原则。

三,索引区分度

我们看2个有序数组

[1,2,3,4,5,6,7,8,8,9,10]

[1,1,1,1,1,8,8,8,8,8]

上面2个数组是有序的,都是10条记录,如果我需要检索值为8的所有记录,那个更快一些?

咱们使用二分法查找包含8的所有记录过程如下:先使用二分法找到最后一个小于8的记录,然后沿着这条记录向后获取下一个记录,和8对比,知道遇到第一个大于8的数字结束,或者到达数组末尾结束。

采用上面这种方法找到8的记录,第一个数组中更快的一些。因为第二个数组中含有8的比例更多的,需要访问以及匹配的次数更多一些。

这里就涉及到数据的区分度问题:

索引区分度 = count(distint 记录) / count(记录)

当索引区分度高的时候,检索数据更快一些,索引区分度太低,说明重复的数据比较多,检索的时候需要访问更多的记录才能够找到所有目标数据。

当索引区分度非常小的时候,基本上接近于全索引数据的扫描了,此时查询速度是比较慢的。

第一个数组索引区分度为1,第二个区分度为0.2,所以第一个检索更快的一些。

所以我们创建索引的时候,尽量选择区分度高的列作为索引。

四,正确使用索引

1,准备400万数据

mysql> drop database if exists test_index;
Query OK, 1 row affected (0.22 sec)

mysql> create database test_index;
Query OK, 1 row affected (0.08 sec)

mysql> use test_index;
mysql> create table test1(
    -> id int not null comment '编号',
    -> name varchar(20) not null comment '姓名',
    -> sex tinyint not null comment '性别',
    -> email varchar(50) comment '油箱');
Query OK, 0 rows affected (0.36 sec)

mysql> drop procedure if exists proce1;
Query OK, 0 rows affected, 1 warning (0.13 sec)

mysql> delimiter $
mysql> create procedure proc1()
    ->  begin
    ->   declare i int default 1;
    ->   start transaction;
    ->   while i <= 4000000 do
    ->    insert into test1 (id, name, sex, email) values (i,concat('bobo',i),if(mod(i,2),1,2),concat('bobo',i,'@qq.com'));
    ->    set i = i + 1;
    ->    if i%10000=0 then
    ->     commit;
    ->     start transaction;
    ->    end if;
    ->  end while;
    ->  commit;
    -> end $
Query OK, 0 rows affected (0.08 sec)

mysql> delimiter ;
mysql> call proc1();
Query OK, 0 rows affected (3 min 25.46 sec)

mysql> select count(*) from test1;
+----------+
| count(*) |
+----------+
|  4000000 |
+----------+
1 row in set (0.63 sec)

mysql> 

上面插入的400万数据,除了sex列,其他列的值都是没有重复的。

2,无索引检索效果

400万数据,我们随便查询几个记录看一下效果。

按照id查询记录:

mysql> select * from test1 where id = 1;
+----+-------+-----+--------------+
| id | name  | sex | email        |
+----+-------+-----+--------------+
|  1 | bobo1 |   1 | bobo1@qq.com |
+----+-------+-----+--------------+
1 row in set (1.86 sec)

id=1的数据,表中只有一行,耗时近2秒,由于id列无索引,只能对400万数据进行全表扫描。

3,主键索引

test1表中没有明确的指定主键,我们将id设置为主键:

mysql> alter table test1 modify id int not null primary key;
Query OK, 0 rows affected (3 min 40.96 sec)
Records: 0  Duplicates: 0  Warnings: 0

mysql> show index from test1;
+-------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+---------+------------+
| Table | Non_unique | Key_name | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment | Visible | Expression |
+-------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+---------+------------+
| test1 |          0 | PRIMARY  |            1 | id          | A         |     3990578 |     NULL |   NULL |      | BTREE      |         |               | YES     | NULL       |
+-------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+---------+------------+
1 row in set (0.03 sec)

id被置为主键之后,会在id上建立聚集索引,随便检索一条我们看一下效果:

mysql> select * from test1 where id = 1;
+----+-------+-----+--------------+
| id | name  | sex | email        |
+----+-------+-----+--------------+
|  1 | bobo1 |   1 | bobo1@qq.com |
+----+-------+-----+--------------+
1 row in set (0.00 sec)

这个速度很快,这个走的是上面介绍的唯一记录检索

4,between and范围检索

mysql> select * from test1 where id between 1000 and 1005;
+------+----------+-----+-----------------+
| id   | name     | sex | email           |
+------+----------+-----+-----------------+
| 1000 | bobo1000 |   2 | bobo1000@qq.com |
| 1001 | bobo1001 |   1 | bobo1001@qq.com |
| 1002 | bobo1002 |   2 | bobo1002@qq.com |
| 1003 | bobo1003 |   1 | bobo1003@qq.com |
| 1004 | bobo1004 |   2 | bobo1004@qq.com |
| 1005 | bobo1005 |   1 | bobo1005@qq.com |
+------+----------+-----+-----------------+
6 rows in set (0.00 sec)

速度也很快,id上有主键索引,这个采用的上面介绍的范围查找可以快速定位目标数据。

但是如果范围太大,跨度的page页太多,速度也会比较慢,如下:

mysql> select count(*) from test1 where id between 1 and 2000000;
+----------+
| count(*) |
+----------+
|  2000000 |
+----------+
1 row in set (0.42 sec)

上面id的值跨度太大,1所在的页和200万所在页中间有很多页需要读取,所以比较慢。

所以使用between and的时候,区间跨度不要太大。

5,in的检索

in方式检索数据,我们还是经常用的。

平时我们做项目的时候,建议少用表连接,比如电商中需要查询订单的信息和订单中商品的名称,可以先查询查询订单表,然后订单表中取出商品的id列表,采用in的方式到商品表检索商品信息,由于商品id是商品表的主键,所以检索速度还是比较快的。

通过id在400万数据中检索10条数据,看看效果:

mysql> select * from test1 where id in (10001,1000001,99999,999999,4000000);
+---------+-------------+-----+--------------------+
| id      | name        | sex | email              |
+---------+-------------+-----+--------------------+
|   10001 | bobo10001   |   1 | bobo10001@qq.com   |
|   99999 | bobo99999   |   1 | bobo99999@qq.com   |
|  999999 | bobo999999  |   1 | bobo999999@qq.com  |
| 1000001 | bobo1000001 |   1 | bobo1000001@qq.com |
| 4000000 | bobo4000000 |   2 | bobo4000000@qq.com |
+---------+-------------+-----+--------------------+
5 rows in set (0.00 sec)

耗时不到1毫秒,还是相当快的。

这个相当于多个分解为多个唯一记录检索,然后将记录合并。

6,多个索引时查询如何走?

我们在name、sex两个字段上分别建个索引

mysql> create index idx1 on test1(name);
Query OK, 0 rows affected (1 min 18.98 sec)
Records: 0  Duplicates: 0  Warnings: 0

mysql> create index idx2 on test1(sex);
Query OK, 0 rows affected (41.86 sec)
Records: 0  Duplicates: 0  Warnings: 0

看一下查询:

mysql> select * from test1 where name='bobo3500000' and sex=2;
+---------+-------------+-----+--------------------+
| id      | name        | sex | email              |
+---------+-------------+-----+--------------------+
| 3500000 | bobo3500000 |   2 | bobo3500000@qq.com |
+---------+-------------+-----+--------------------+
1 row in set (0.00 sec)

上面查询速度很快,name和sex上各有一个索引,觉得上面走哪个索引?

有人说name位于where第一个,所以走的是name字段所在的索引,过程可以解释为这样:

  1. 走name所在的索引找到bobo3500000对应的所有记录
  2. 遍历记录过滤出sex=2的值

我们看一下name='bobo3500000'检索速度,确实很快,如下:

mysql> select * from test1 where name='bobo3500000';
+---------+-------------+-----+--------------------+
| id      | name        | sex | email              |
+---------+-------------+-----+--------------------+
| 3500000 | bobo3500000 |   2 | bobo3500000@qq.com |
+---------+-------------+-----+--------------------+
1 row in set (0.00 sec)

走name索引,然后再过滤,确实可以,速度也很快,果真和where后字段顺序有关么?我们把name和sex的顺序对调一下,如下:

mysql> select * from test1 where sex=2 and name='bobo3500000';
+---------+-------------+-----+--------------------+
| id      | name        | sex | email              |
+---------+-------------+-----+--------------------+
| 3500000 | bobo3500000 |   2 | bobo3500000@qq.com |
+---------+-------------+-----+--------------------+
1 row in set (0.00 sec)

速度还是很快,这次是不是先走sex索引检索出数据,然后再过滤name呢?我们先来看一下sex=2查询速度:

mysql> select count(id) from test1 where sex=2;
+-----------+
| count(id) |
+-----------+
|   2000000 |
+-----------+
1 row in set (0.31 sec)

看上面,查询耗时310毫秒,200万数据,如果走sex肯定是不行的。

我们使用explain来看一下:

mysql> explain select * from test1 where sex=2 and name='bobo3500000';
+----+-------------+-------+------------+------+---------------+------+---------+-------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key  | key_len | ref   | rows | filtered | Extra       |
+----+-------------+-------+------------+------+---------------+------+---------+-------+------+----------+-------------+
|  1 | SIMPLE      | test1 | NULL       | ref  | idx1,idx2     | idx1 | 82      | const |    1 |    50.00 | Using where |
+----+-------------+-------+------------+------+---------------+------+---------+-------+------+----------+-------------+
1 row in set, 1 warning (0.00 sec)

possible_keys:列出了这个查询可能会走两个索引(idx1、idx2)实际上走的却是idx1(key列:实际走的索引)。

当多个条件中有索引的时候,并且关系是and的时候,会走索引区分度高的,显然name字段重复度很低,走name查询会更快一些。

7,模糊查询

mysql> select count(*) from test1 where name like 'bobo1000%';
+----------+
| count(*) |
+----------+
|     1111 |
+----------+
1 row in set (0.00 sec)

mysql> select count(*) from test1 where name like '%bobo1000%';
+----------+
| count(*) |
+----------+
|     1111 |
+----------+
1 row in set (1.13 sec)

上面第一个查询可以利用到name字段上面的索引,下面的查询是无法确定需要查找的值所在的范围的,只能全表扫描,无法利用索引,所以速度比较慢,这个过程上面有说过。

8,回表

当需要查询的数据在索引树中不存在的时候,需要再次到聚集索引中去获取,这个过程叫做回表,如查询:

mysql> select * from test1 where name='bobo123456';
+--------+------------+-----+-------------------+
| id     | name       | sex | email             |
+--------+------------+-----+-------------------+
| 123456 | bobo123456 |   2 | bobo123456@qq.com |
+--------+------------+-----+-------------------+
1 row in set (0.00 sec)

上面查询是*,由于name列所在的索引中只有name、id两个列的值,不包含sex、email,所以上面过程如下:

  1. 走name索引检索bobo123456对应的记录,取出id为123456
  2. 在主键索引中检索出id=123456的记录,获取所有字段的值

9,索引覆盖

查询中采用的索引树中包含了查询所需要的所有字段的值,不需要再去聚集索引检索数据,这种叫索引覆盖。

我们来看一个查询:

mysql> select id,name from test1 where name='bobo123456';
+--------+------------+
| id     | name       |
+--------+------------+
| 123456 | bobo123456 |
+--------+------------+
1 row in set (0.00 sec)

name对应idx1索引,id为主键,所以idx1索引树叶子节点中包含了name、id的值,这个查询只用走idx1这一个索引就可以了,如果select后面使用*,还需要一次回表获取sex、email的值。

所以写sql的时候,尽量避免使用**可能会多一次回表操作,需要看一下是否可以使用索引覆盖来实现,效率更高一些。

10,索引下推

简称ICP,Index Condition Pushdown(ICP)是MySQL 5.6中新特性,是一种在存储引擎层使用索引过滤数据的一种优化方式,ICP可以减少存储引擎访问基表的次数以及MySQL服务器访问存储引擎的次数。

举个例子来说一下:

我们需要查询name以bobo35开头的,性别为1的记录数,sql如下:

mysql> select count(id) from test1 where name like 'bobo35%' and sex=1;
+-----------+
| count(id) |
+-----------+
|     55556 |
+-----------+
1 row in set (0.16 sec)

过程:

  1. 走name索引检索出以bobo35的第一条记录,得到记录的id
  2. 利用id去主键索引中查询出这条记录R1
  3. 判断R1中的sex是否为1,然后重复上面的操作,直到找到所有记录为止。

上面的过程中需要走name索引以及需要回表操作。

如果采用ICP的方式,我们可以这么做,创建一个(name,sex)的组合索引,查询过程如下:

  1. 走(name,sex)索引检索出以bobo35的第一条记录,可以得到(name,sex,id),记做R1
  2. 判断R1.sex是否为1,然后重复上面的操作,知道找到所有记录为止

这个过程中不需要回表操作了,通过索引的数据就可以完成整个条件的过滤,速度比上面的更快一些。

11,数字使字符串类索引失效

mysql> insert into test1 (id,name,sex,email) values (4000001,'1',1,'[email protected]');
Query OK, 1 row affected (0.09 sec)

mysql> select * from test1 where name='1';
+---------+------+-----+--------------------+
| id      | name | sex | email              |
+---------+------+-----+--------------------+
| 4000001 | 1    |   1 | bobo4000001@qq.com |
+---------+------+-----+--------------------+
1 row in set (0.00 sec)

mysql> select * from test1 where name = 1;
+---------+------+-----+--------------------+
| id      | name | sex | email              |
+---------+------+-----+--------------------+
| 4000001 | 1    |   1 | bobo4000001@qq.com |
+---------+------+-----+--------------------+
1 row in set, 65535 warnings (1.86 sec)

上面3条sql,我们插入了一条记录。

第二条查询很快,第三条用name和1比较,name上有索引,name是字符串类型,字符串和数字比较的时候,会将字符串强制转换为数字,然后进行比较,所以第二个查询变成了全表扫描,只能取出每条数据,将name转换为数字和1进行比较。

数字字段和字符串比较什么效果呢?如下:

mysql> select * from test1 where id = '4000000';
+---------+-------------+-----+--------------------+
| id      | name        | sex | email              |
+---------+-------------+-----+--------------------+
| 4000000 | bobo4000000 |   2 | bobo4000000@qq.com |
+---------+-------------+-----+--------------------+
1 row in set (0.00 sec)

mysql> select * from test1 where id = 4000000;
+---------+-------------+-----+--------------------+
| id      | name        | sex | email              |
+---------+-------------+-----+--------------------+
| 4000000 | bobo4000000 |   2 | bobo4000000@qq.com |
+---------+-------------+-----+--------------------+
1 row in set (0.00 sec)

id上面有主键索引,id是int类型的,可以看到,上面两个查询都非常快,都可以正常利用索引快速检索,所以如果字段是数组类型的,查询的值是字符串还是数组都会走索引。

12,函数使索引无效

mysql> select a.name+1 from test1 a where a.name = 'bobo1';
+----------+
| a.name+1 |
+----------+
|        1 |
+----------+
1 row in set, 1 warning (0.00 sec)

mysql> select * from test1 a where concat(a.name,'1') = 'bobo11';
+----+-------+-----+--------------+
| id | name  | sex | email        |
+----+-------+-----+--------------+
|  1 | bobo1 |   1 | bobo1@qq.com |
+----+-------+-----+--------------+
1 row in set (1.28 sec)

name上有索引,上面查询,第一个走索引,第二个不走索引,第二个使用了函数之后,name所在的索引树是无法快速定位需要查找的数据所在的页的,只能将所有页的记录加载到内存中,然后对每条数据使用函数进行计算之后再进行条件判断,此时索引无效了,变成了全表数据扫描。

结论:索引字段使用函数查询使索引无效。

13,运算符使索引无效

mysql> select * from test1 where id = 2 - 1;
+----+-------+-----+--------------+
| id | name  | sex | email        |
+----+-------+-----+--------------+
|  1 | bobo1 |   1 | bobo1@qq.com |
+----+-------+-----+--------------+
1 row in set (0.00 sec)

mysql> select * from test1 where id + 1 = 2;
+----+-------+-----+--------------+
| id | name  | sex | email        |
+----+-------+-----+--------------+
|  1 | bobo1 |   1 | bobo1@qq.com |
+----+-------+-----+--------------+
1 row in set (0.97 sec)

id上有主键索引,上面查询,第一个走索引,第二个不走索引,第二个使用运算符,id所在的索引树是无法快速定位需要查找的数据所在的页的,只能将所有页的记录加载到内存中,然后对每条数据的id进行计算之后再判断是否等于1,此时索引无效了,变成了全表数据扫描。

结论:索引字段使用了运算符将使索引无效。

14,使用索引优化排序

我们有个订单表t_order(id,user_id,addtime,price),经常会查询某个用户的订单,并且按照addtime升序排序,应该怎么创建索引呢?我们来分析一下。

在user_id上创建索引,我们分析一下这种情况,数据检索的过程:

  1. 走user_id索引,找到记录的的id
  2. 通过id在主键索引中回表检索出整条数据
  3. 重复上面的操作,获取所有目标记录
  4. 在内存中对目标记录按照addtime进行排序

我们要知道当数据量非常大的时候,排序还是比较慢的,可能会用到磁盘中的文件,有没有一种方式,查询出来的数据刚好是排好序的。

我们再回顾一下mysql中b+树数据的结构,记录是按照索引的值排序组成的链表,如果将user_id和addtime放在一起组成联合索引(user_id,addtime),这样通过user_id检索出来的数据自然就是按照addtime排好序的,这样直接少了一步排序操作,效率更好,如果需addtime降序,只需要将结果翻转一下就可以了。

使用联合索引排序时需要注意几点:

1,使用范围查寻会使后面的索引失效

mysql> explain select * from t_order where user_id>5 order by addtime asc;
+----+-------------+---------+------------+-------+---------------+------+---------+------+------+----------+---------------------------------------+
| id | select_type | table   | partitions | type  | possible_keys | key  | key_len | ref  | rows | filtered | Extra                                 |
+----+-------------+---------+------------+-------+---------------+------+---------+------+------+----------+---------------------------------------+
|  1 | SIMPLE      | t_order | NULL       | range | idx1          | idx1 | 5       | NULL |    1 |   100.00 | Using index condition; Using filesort |
+----+-------------+---------+------------+-------+---------------+------+---------+------+------+----------+---------------------------------------+
1 row in set, 1 warning (0.00 sec)

上面执行计划可以看出,user_id>5使用了索引,但是后面的 addtime就无法使用索引了,因为user_id使用的是范围查询,导致后面的索引失效。

2,order by后面如果升降序不一致,也不会走索引排序

mysql> create index idx2 on t_order(user_id,addtime,price);
Query OK, 0 rows affected (0.36 sec)
Records: 0  Duplicates: 0  Warnings: 0


mysql> explain select * from t_order where user_id=1 order by addtime asc,price asc;
+----+-------------+---------+------------+------+---------------+------+---------+-------+------+----------+-------------+
| id | select_type | table   | partitions | type | possible_keys | key  | key_len | ref   | rows | filtered | Extra       |
+----+-------------+---------+------------+------+---------------+------+---------+-------+------+----------+-------------+
|  1 | SIMPLE      | t_order | NULL       | ref  | idx1,idx2     | idx2 | 5       | const |    1 |   100.00 | Using index |
+----+-------------+---------+------------+------+---------------+------+---------+-------+------+----------+-------------+
1 row in set, 1 warning (0.01 sec)

mysql> explain select * from t_order where user_id=1 order by addtime asc,price desc;
+----+-------------+---------+------------+------+---------------+------+---------+-------+------+----------+----------------+
| id | select_type | table   | partitions | type | possible_keys | key  | key_len | ref   | rows | filtered | Extra          |
+----+-------------+---------+------------+------+---------------+------+---------+-------+------+----------+----------------+
|  1 | SIMPLE      | t_order | NULL       | ref  | idx1,idx2     | idx1 | 5       | const |    1 |   100.00 | Using filesort |
+----+-------------+---------+------------+------+---------------+------+---------+-------+------+----------+----------------+
1 row in set, 1 warning (0.00 sec)

因为mysql索引是B+树,本身是升序排列,如果查询时指定全部生序全部降序都是可以利用索引排序的,无非是从后面遍历还是前面遍历,但是你如果指定组合索引中一个升序,一个降序,那么不好意思,就无法利用索引进行排序了。

五,总结

  1. 在区分度高的字段上面建立索引可以有效的使用索引,区分度太低,无法有效的利用索引,可能需要扫描所有数据页,此时和不使用索引差不多
  2. 联合索引注意最左匹配原则:必须按照从左到右的顺序匹配,mysql会一直向右匹配直到遇到范围查询(>、<、between、like)就停止匹配,比如a = 1 and b = 2 and c > 3 and d = 4 如果建立(a,b,c,d)顺序的索引,d是用不到索引的,如果建立(a,b,d,c)的索引则都可以用到,a,b,d的顺序可以任意调整
  3. 查询记录的时候,少使用*,尽量去利用索引覆盖,可以减少回表操作,提升效率
  4. 有些查询可以采用联合索引,进而使用到索引下推(IPC),也可以减少回表操作,提升效率
  5. 禁止对索引字段使用函数、运算符操作,会使索引失效
  6. 字符串字段和数字比较的时候会使索引无效
  7. 模糊查询’%值%'会使索引无效,变为全表扫描,但是’值%'这种可以有效利用索引
  8. 排序中尽量使用到索引字段,这样可以减少排序,提升查询效率

猜你喜欢

转载自blog.csdn.net/u013277209/article/details/113514316
今日推荐