Mysql 索引的学习与实际测试

216 阅读7分钟

环境

  1. 操作系统 Win10 64bit
  2. Mysql 版本 5.7.14 数据库字符集 utf8mb4 排序规则 utf8mb4_general_ci

测试数据准备

建表脚本

CREATE TABLE `person`  (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `sex` varchar(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `birthdate` date NULL DEFAULT NULL,
  `status` tinyint(255) NULL DEFAULT NULL,
  `money` decimal(10, 2) NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;

写程序插入数据 529717 条,插入时内容如下:

  • id 自增主键
  • name 随机生成的长度为 4 的字符串
  • sex 插入值轮流设置为 m 或者 f
  • birthdate 插入 2003-11-052019-06-15 之间的任意一天
  • status 每100条记录写入1个值 1 和99个值 2
  • money 1~1000000 之间的任意数字,两位小数

前10条数据概览:

mysql> select * from person limit 10;
+----+------+------+------------+--------+-----------+
| id | name | sex  | birthdate  | status | money     |
+----+------+------+------------+--------+-----------+
|  1 | gnFV | m    | 2018-05-26 |      1 | 724065.50 |
|  2 | hvDT | f    | 2004-02-18 |      2 |  72224.01 |
|  3 | Ajyu | m    | 2003-12-30 |      2 | 383774.51 |
|  4 | Usxj | f    | 2019-05-01 |      2 | 946915.34 |
|  5 | Euhs | m    | 2017-12-03 |      2 | 564437.18 |
|  6 | GOWk | f    | 2016-05-29 |      2 |  57537.67 |
|  7 | CoZd | m    | 2007-09-15 |      2 | 421090.69 |
|  8 | eQOx | f    | 2011-08-14 |      2 | 578494.77 |
|  9 | ZwCk | m    | 2013-10-26 |      2 | 604131.59 |
| 10 | rxyG | f    | 2010-05-04 |      2 | 790983.80 |
+----+------+------+------------+--------+-----------+
10 rows in set (0.00 sec)

测试过程

主键索引的使用

依据ID查询单条记录,速度非常快,耗时几乎为0

mysql> select * from person where id = 1000;
+------+------+------+------------+--------+-----------+
| id   | name | sex  | birthdate  | status | money     |
+------+------+------+------------+--------+-----------+
| 1000 | iYcE | f    | 2006-12-10 |      2 | 253368.78 |
+------+------+------+------------+--------+-----------+
1 row in set (0.00 sec)

执行计划如下,可以看到使用了主键索引。

mysql> explain select * from person where id = 1000;
+----+-------------+--------+------------+-------+---------------+---------+---------+-------+------+----------+-------+
| id | select_type | table  | partitions | type  | possible_keys | key     | key_len | ref   | rows | filtered | Extra |
+----+-------------+--------+------------+-------+---------------+---------+---------+-------+------+----------+-------+
|  1 | SIMPLE      | person | NULL       | const | PRIMARY       | PRIMARY | 4       | const |    1 |   100.00 | NULL  |
+----+-------------+--------+------------+-------+---------------+---------+---------+-------+------+----------+-------+
1 row in set, 1 warning (0.00 sec)

普通索引对查询效率的提升

在没有索引的情况下,先使用 name 给定条件进行查询

mysql> select * from person where name = 'iYcE';
+------+------+------+------------+--------+-----------+
| id   | name | sex  | birthdate  | status | money     |
+------+------+------+------------+--------+-----------+
| 1000 | iYcE | f    | 2006-12-10 |      2 | 253368.78 |
+------+------+------+------------+--------+-----------+
1 row in set (0.30 sec)

name 列加上索引

mysql> ALTER TABLE `test`.`person` ADD INDEX `index_name`(`name`) USING BTREE;
Query OK, 0 rows affected (1.32 sec)
Records: 0  Duplicates: 0  Warnings: 0

执行同样的查询

mysql> select * from person where name = 'iYcE';
+------+------+------+------------+--------+-----------+
| id   | name | sex  | birthdate  | status | money     |
+------+------+------+------------+--------+-----------+
| 1000 | iYcE | f    | 2006-12-10 |      2 | 253368.78 |
+------+------+------+------------+--------+-----------+
1 row in set (0.00 sec)

查询耗时 0.30 -> 0.00

mysql> explain select * from person where name = 'iYcE';
+----+-------------+--------+------------+------+---------------+------------+---------+-------+------+----------+-------+
| id | select_type | table  | partitions | type | possible_keys | key        | key_len | ref   | rows | filtered | Extra |
+----+-------------+--------+------------+------+---------------+------------+---------+-------+------+----------+-------+
|  1 | SIMPLE      | person | NULL       | ref  | index_name    | index_name | 1023    | const |    1 |   100.00 | NULL  |
+----+-------------+--------+------------+------+---------------+------------+---------+-------+------+----------+-------+
1 row in set, 1 warning (0.00 sec)

看一下执行计划,确实使用到了索引。
关于执行计划的详细解释可以看一下这篇文章 www.jianshu.com/p/514aa0b13…

查询语句是否能够命中索引

先看 like 的情况

mysql> explain select * from person where name like '%YcE%';
+----+-------------+--------+------------+------+---------------+------+---------+------+--------+----------+-------------+
| id | select_type | table  | partitions | type | possible_keys | key  | key_len | ref  | rows   | filtered | Extra       |
+----+-------------+--------+------------+------+---------------+------+---------+------+--------+----------+-------------+
|  1 | SIMPLE      | person | NULL       | ALL  | NULL          | NULL | NULL    | NULL | 528768 |    11.11 | Using where |
+----+-------------+--------+------------+------+---------------+------+---------+------+--------+----------+-------------+

mysql> explain select * from person where name like '%YcE';
+----+-------------+--------+------------+------+---------------+------+---------+------+--------+----------+-------------+
| id | select_type | table  | partitions | type | possible_keys | key  | key_len | ref  | rows   | filtered | Extra       |
+----+-------------+--------+------------+------+---------------+------+---------+------+--------+----------+-------------+
|  1 | SIMPLE      | person | NULL       | ALL  | NULL          | NULL | NULL    | NULL | 528768 |    11.11 | Using where |
+----+-------------+--------+------------+------+---------------+------+---------+------+--------+----------+-------------+

mysql> explain select * from person where name like 'YcE%';
+----+-------------+--------+------------+-------+---------------+------------+---------+------+------+----------+-----------------------+
| id | select_type | table  | partitions | type  | possible_keys | key        | key_len | ref  | rows | filtered | Extra
           |
+----+-------------+--------+------------+-------+---------------+------------+---------+------+------+----------+-----------------------+
|  1 | SIMPLE      | person | NULL       | range | index_name    | index_name | 1023    | NULL |   29 |   100.00 | Using index condition |
+----+-------------+--------+------------+-------+---------------+------------+---------+------+------+----------+-----------------------+

mysql> explain select * from person where name not like 'YcE%';
+----+-------------+--------+------------+------+---------------+------+---------+------+--------+----------+-------------+
| id | select_type | table  | partitions | type | possible_keys | key  | key_len | ref  | rows   | filtered | Extra       |
+----+-------------+--------+------------+------+---------------+------+---------+------+--------+----------+-------------+
|  1 | SIMPLE      | person | NULL       | ALL  | NULL          | NULL | NULL    | NULL | 528768 |    88.89 | Using where |
+----+-------------+--------+------------+------+---------------+------+---------+------+--------+----------+-------------+

可以看到,前模糊查询无法使用索引,而后模糊查询可以使用索引。而 not like 查询未使用索引。

mysql> explain select * from person where name in ('iYcE','wWYp');
+----+-------------+--------+------------+-------+---------------+------------+---------+------+------+----------+-----------------------+
| id | select_type | table  | partitions | type  | possible_keys | key        | key_len | ref  | rows | filtered | Extra
           |
+----+-------------+--------+------------+-------+---------------+------------+---------+------+------+----------+-----------------------+
|  1 | SIMPLE      | person | NULL       | range | index_name    | index_name | 1023    | NULL |    3 |   100.00 | Using index condition |
+----+-------------+--------+------------+-------+---------------+------------+---------+------+------+----------+-----------------------+

mysql> explain select * from person where name not in ('iYcE','wWYp');
+----+-------------+--------+------------+------+---------------+------+---------+------+--------+----------+-------------+
| id | select_type | table  | partitions | type | possible_keys | key  | key_len | ref  | rows   | filtered | Extra       |
+----+-------------+--------+------------+------+---------------+------+---------+------+--------+----------+-------------+
|  1 | SIMPLE      | person | NULL       | ALL  | index_name    | NULL | NULL    | NULL | 528768 |   100.00 | Using where |
+----+-------------+--------+------------+------+---------------+------+---------+------+--------+----------+-------------+

使用 in 查询命中了索引,而 not in 无法命中索引。

现在我们给 money 列加上单列索引,然后继续。

mysql> explain select * from person where money > 100000;
+----+-------------+--------+------------+-------+---------------+-------------+---------+------+--------+----------+----------------------------------+
| id | select_type | table  | partitions | type  | possible_keys | key         | key_len | ref  | rows   | filtered | Extra
                         |
+----+-------------+--------+------------+-------+---------------+-------------+---------+------+--------+----------+----------------------------------+
|  1 | SIMPLE      | person | NULL       | range | index_money   | index_money | 6       | NULL | 264384 |   100.00 | Using index condition; Using MRR |
+----+-------------+--------+------------+-------+---------------+-------------+---------+------+--------+----------+----------------------------------+

mysql> explain select * from person where money <= 400000;
+----+-------------+--------+------------+-------+---------------+-------------+---------+------+--------+----------+----------------------------------+
| id | select_type | table  | partitions | type  | possible_keys | key         | key_len | ref  | rows   | filtered | Extra
                         |
+----+-------------+--------+------------+-------+---------------+-------------+---------+------+--------+----------+----------------------------------+
|  1 | SIMPLE      | person | NULL       | range | index_money   | index_money | 6       | NULL | 264384 |   100.00 | Using index condition; Using MRR |
+----+-------------+--------+------------+-------+---------------+-------------+---------+------+--------+----------+----------------------------------+

mysql> explain select * from person where money > 100000 and money <= 400000;
+----+-------------+--------+------------+-------+---------------+-------------+---------+------+--------+----------+----------------------------------+
| id | select_type | table  | partitions | type  | possible_keys | key         | key_len | ref  | rows   | filtered | Extra
                         |
+----+-------------+--------+------------+-------+---------------+-------------+---------+------+--------+----------+----------------------------------+
|  1 | SIMPLE      | person | NULL       | range | index_money   | index_money | 6       | NULL | 264384 |   100.00 | Using index condition; Using MRR |
+----+-------------+--------+------------+-------+---------------+-------------+---------+------+--------+----------+----------------------------------+

mysql> explain select * from person where money between 100000 and 400000;
+----+-------------+--------+------------+-------+---------------+-------------+---------+------+--------+----------+----------------------------------+
| id | select_type | table  | partitions | type  | possible_keys | key         | key_len | ref  | rows   | filtered | Extra
                         |
+----+-------------+--------+------------+-------+---------------+-------------+---------+------+--------+----------+----------------------------------+
|  1 | SIMPLE      | person | NULL       | range | index_money   | index_money | 6       | NULL | 264384 |   100.00 | Using index condition; Using MRR |
+----+-------------+--------+------------+-------+---------------+-------------+---------+------+--------+----------+----------------------------------+

mysql> explain select * from person where money not between 100000 and 400000;
+----+-------------+--------+------------+------+---------------+------+---------+------+--------+----------+-------------+
| id | select_type | table  | partitions | type | possible_keys | key  | key_len | ref  | rows   | filtered | Extra       |
+----+-------------+--------+------------+------+---------------+------+---------+------+--------+----------+-------------+
|  1 | SIMPLE      | person | NULL       | ALL  | index_money   | NULL | NULL    | NULL | 528768 |    70.08 | Using where |
+----+-------------+--------+------------+------+---------------+------+---------+------+--------+----------+-------------+

mysql> explain select * from person where money < 100000 or money > 400000;
+----+-------------+--------+------------+------+---------------+------+---------+------+--------+----------+-------------+
| id | select_type | table  | partitions | type | possible_keys | key  | key_len | ref  | rows   | filtered | Extra       |
+----+-------------+--------+------------+------+---------------+------+---------+------+--------+----------+-------------+
|  1 | SIMPLE      | person | NULL       | ALL  | index_money   | NULL | NULL    | NULL | 528768 |    70.08 | Using where |
+----+-------------+--------+------------+------+---------------+------+---------+------+--------+----------+-------------+

mysql> explain select * from person where money > 100000 or money < 400000;
+----+-------------+--------+------------+-------+---------------+-------------+---------+------+--------+----------+----------------------------------+
| id | select_type | table  | partitions | type  | possible_keys | key         | key_len | ref  | rows   | filtered | Extra
                         |
+----+-------------+--------+------------+-------+---------------+-------------+---------+------+--------+----------+----------------------------------+
|  1 | SIMPLE      | person | NULL       | range | index_money   | index_money | 6       | NULL | 264384 |   100.00 | Using index condition; Using MRR |
+----+-------------+--------+------------+-------+---------------+-------------+---------+------+--------+----------+----------------------------------+

可以看到,<= >= > < between 等运算符,以及 and 连接条件都成功命中了索引,而 not between 同样无法命中索引。最后两个 or 比较有意思,前一个显示没有命中索引,后一个显示命中了索引,其实后一个实际效果相当于没有 where 条件吧,这里不太清楚为什么

mysql> explain select * from person where money + 10000 > 510000;
+----+-------------+--------+------------+------+---------------+------+---------+------+--------+----------+-------------+
| id | select_type | table  | partitions | type | possible_keys | key  | key_len | ref  | rows   | filtered | Extra       |
+----+-------------+--------+------------+------+---------------+------+---------+------+--------+----------+-------------+
|  1 | SIMPLE      | person | NULL       | ALL  | NULL          | NULL | NULL    | NULL | 528768 |   100.00 | Using where |
+----+-------------+--------+------------+------+---------------+------+---------+------+--------+----------+-------------+

请注意,对于字段进行运算是无法命中索引的

索引一定能够提高查询效率吗

看下面的示例,此时 sex 列未加索引

select id from person where sex = 'm'
> OK
> 时间: 0.264s

select id,name from person where sex = 'm'
> OK
> 时间: 0.328s

sex 列加上索引之后呢

select id from person where sex = 'm'
> OK
> 时间: 0.138s

select id,name from person where sex = 'm'
> OK
> 时间: 3.519s

纳尼???可以看到,sex 列加上索引之后,对于结果只包含主键 id 列的查询来说,效率有提升,但是提升并不明显,而对于查询结果除了包含主键 id 还包含 name 的查询来说,效率大幅降低了

  • 对于可能出现的数值比较少的列,如 sex 列,索引带来的查询效率提升很小。就是说当表里的数据非常多的时候,每一种值对应的数据量都很多,那么这个列对数据筛选的帮助就不大。因此,对于性别、状态等值多为枚举(男/女)或(0/1)的字段来说,可以不必创建索引。
  • 对于查询结果数量比较多的情况下,如此例,查询会返回表中 1/2 的数据量。那么这时,使用索引的情况下会存在一个回表查询的操作(即从 sex 的索引结构中获取到指针,还需要回到 person 表中将 name 信息查询出来),这样的效率可能还不如直接到 person 表中全表扫描查询,因此耗时更长了(此部分解释为笔者个人理解,可能并不准确)。
-- status列未加索引
select id from person where status = 1
> OK
> 时间: 0.362s

select id from person where status = 2
> OK
> 时间: 0.426s

select id,`name` from person where status = 1
> OK
> 时间: 0.336s

select id,`name` from person where status = 2
> OK
> 时间: 0.524s

-- status列加索引
select id from person where status = 1
> OK
> 时间: 0.016s

select id from person where status = 2
> OK
> 时间: 0.328s

select id,`name` from person where status = 1
> OK
> 时间: 0.071s

select id,`name` from person where status = 2
> OK
> 时间: 7.438s

未完待续