MySQL 系列1 —— 索引

275 阅读11分钟

MySQL 索引

索引在 MySQL 中页叫做"键(key)",是存储引擎用于快速找到记录的一种数据结构。

一、索引类型

在 MySQL 中,索引是在存储引擎层实现的,所以不同存储引擎即使支持同一类型索引,其实现方式也可能是不同的。

常见类型有:

  • B-Tree 索引
  • 哈希索引(hash index)
  • 空间数据索引(R-Tree)
  • 全文索引

二、索引优点

1.索引减少了服务器需要扫描的数据量

2.索引可以帮助服务器避免排序和临时表

3.索引可以将随机I/O变为顺序I/O

三、如何创建高性能索引(InnoDB)

1.独立的列

索引列不能是表达式的一部分,也不能是函数的参数。

例如,下面则无法使用 user_id 列的索引。

SELECT name FROM user WHERE user_id + 1 = 5;
SELECT name FROM user WHERE FROM_UNIXTIME(created_at,format) = '2010-09-10';

究其原因是因为B+Tree 数据结构查找数据是从根节点开始查找,根节点存储了索引的值,所以可以通过二分查找快速定位要需要的行,但是加上了表达式,则 MySQL 不得不一行一行的扫描然后计算判断。

2.前缀索引和索引选择性

有时候我们需要为一个字符串创建索引,这时候可以使用 hash index,但有时候这样还不够,如果字符串非常长,如果整个字符串作为索引则非常占用空间,此时我们需要通过计算索引的选择性来创建前缀索引。

如 name 列里有一个叫 ZhangSan 的名字,如果按照长度5创建前缀索引,那么 MySQL 能索引到的部分就是 Zhang。那一个列我们到底应该创建多长的前缀索引才是最好的了?这时候就要用到索引的选择性这个知识点。

索引的选择性 :不重复的索引值(也成为基数)和数据表的记录总数(T)的比值。 范围在1/T到1之间。选择性越高查询效率越高。

如何计算了:

a.先计算完整列的索引选择性:

mysql> SELECT COUNT(DISTINCT clsbdh)/COUNT(*) FROM vehicles;
+---------------------------------+
| COUNT(DISTINCT clsbdh)/COUNT(*) |
+---------------------------------+
|                          0.9639 |
+---------------------------------+

b.计算不同长度前缀列的索引选择性:

mysql> SELECT COUNT(DISTINCT LEFT(clsbdh,13))/COUNT(*) AS sel13,
    -> COUNT(DISTINCT LEFT(clsbdh,14))/COUNT(*) AS sel14,
    -> COUNT(DISTINCT LEFT(clsbdh,15))/COUNT(*) AS sel15,
    -> COUNT(DISTINCT LEFT(clsbdh,16))/COUNT(*) AS sel16,
    -> COUNT(DISTINCT LEFT(clsbdh,17))/COUNT(*) AS sel17
    ->  FROM vehicles;
+--------+--------+--------+--------+--------+
| sel13  | sel14  | sel15  | sel16  | sel17  |
+--------+--------+--------+--------+--------+
| 0.0277 | 0.0332 | 0.0423 | 0.1262 | 0.9639 |
+--------+--------+--------+--------+--------+

c.从 b 步奏的计算结果中找一个与 a 步奏计算结果最接近的值,这里发现长度为17时符合。这个是非常接近于1,说明该列数据大部分都不重复。

d.创建前缀索引

ALTER TABLE vehicles  ADD KEY (clsbdh(17));

那如果需要后缀索引该怎么办了?比如要查找 @qq.com 域名的所有电子邮件地址。此时我们可以把字符串反转后存储,然后基于此创建前缀索引 KEY(col(7)),因为反转后moc.qq@长度为7。

3.联合索引

联合索引和多个独立的索引这两者经常被人搞混,我们先来看看这张表:


CREATE TABLE `user` (
  `id` int(11) NOT NULL,
  `name` varchar(32) DEFAULT NULL,
  `age` int(11) DEFAULT NULL,
  `class` varchar(20),
  `last_name` varchar(30),
  PRIMARY KEY (`id`),
  KEY `idx_age` (`age`),
  KEY `idx_name_age` (`name`,`age`)
) ENGINE=InnoDB

如上, idx_age 是独立的索引, idx_name_age 是联合索引。为 WHERE 条件里的列都单独创建一个索引其实是不太好,这样没办法优化。

联合索引 key(name,age) 的数据结构B+Tree示例图(参考)

可以看到根节点存储了键值,并且键值先按照 age 升序排列, age列相同时按照 name 排序,同时叶子节点存储了主键的值 ID1 、ID2等。

回表

对于下面的语句,执行过程:

select * from user age = 11 and name = 'aab';

首先查找二级索引找到目标列,然后得到主键值为:ID1,此时在通过主键值到主键索引去查找到对应的行,读取对应行的数据。回到主键去查找的过程称为 回表

使用联合索引要注意 最左前缀原则

假设现在索引列为 key(age,name,class):

select * from user where age = 11 and name = 'aab' and class = '三年级一班'select * from user where age = 11 and name = 'aab'select * from user where age = 11select * from user where age = 11 and name like '%a'

以上语句都能使用到索引,因为都是按照联合索引创建顺序来查找的。而下面的语句则用不到索引:

-- 用不到任何索引
select * from user where name = 'aab' and class = '三年级一班'select * from user where class = '三年级一班'-- 只有第一列能用到索引
select * from user where age = 11 and class = '三年级一班'-- class 列用不到索引,因为 name 列使用了范围查询,>、< 也算是范围
select * from user where age = 11 and name like '%a' and class = '三年级一班';

所以有需要范围查询的列,我们就把索引安排在最后即可。

其实回头查看下联合索引的数据结构思考就能明白,因为索引创建的时候已经是排好序的,第一列相同的情况下,会按照第二列排序,然后是第三列。所以查找的时候只能从第一列开始查找,如果改变顺序,则需要全表扫描。

按照最左匹配原则下面的语句能用到索引吗?

select * from user where name = 'aab' and age =  11 and class = '三年级一班';

答案是能用到索引,不信请看执行结果:

mysql> explain select * from user where name = 'aab' and age =  11 and class = '三年级一班'\G;
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        tableuser
   partitions: NULL
         type: ref
possible_keys: idx
          key: idx
      key_len: 248
          ref: const,const,const
         rows1
     filtered: 100.00
        Extra: Using index

这... 不是说要符合最左前缀原则才能使用索引吗?我真的方了。

其实原因在于 MySQL 查询优化器帮我们调整了顺序,实际执行的语句还是如下:

select * from user where age = 11 and name = 'aab' and class = '三年级一班'

经过实验结果,只有第一列和第二列会帮我们优化,后面的列还是要自己注意顺序。

索引下推

那对于不符合最左前缀的列是怎么处理的了:

select * from user where age = 11 and class = '三年级一班'

在MySQL5.6之后引入的索引下推优化(index condition pushdown), 可以在索引遍历过程中,对索引中包含的字段先做判断,直接过滤掉不满足条件的记录,减少回表次数。如果条件里面里面不含有索引列,那还是需要回到主键索引去搜索。

4.选择合适的索引列顺序

还是以上面的表为例子,我们可以创建索引 KEY(name,age) 或者 KEY(age,name)。哪种最好了?我们此时可以看哪个列的选择性更高。

mysql> SELECT COUNT(DISTINCT name) / COUNT(*AS name_selectivity
 , COUNT(DISTINCT age) / COUNT(*AS age_selectivity
 , COUNT(*FROM user;
+------------------+-----------------+----------+
| name_selectivity | age_selectivity | count(*|
+------------------+--------+--------+----------+
|     0.0005       |      0.9962     |  2945454 |
+------------------+-----------------+----------+

age 的选择性更高,应该创建索引 KEY(name,age)。

5.聚簇索引

聚簇索引并不是一种单独的索引类型,而是一种数据存储方式。“聚簇”表示数据行和相邻的键值紧凑地存储在一起。InnoDB 的聚簇索引就是主键。如果表没有定义主键,InnoDB会选择一个唯一的非空索引代替。如果没有这样的索引,InnoDB 会隐式定义一个主键来作为聚簇索引。

我们尽量避免使用随机的(不连续切值得分布范围非常大)聚簇索引。例如使用 UUID 来作为聚簇索引。它会让聚簇索引的插入变得完全随机。同时UUID值非常长,索引占用空间也会变大。

6.覆盖索引

索引包含所有需要查询的字段的值,我们就成为覆盖索引。

假设此时联合索引为:key(age,name,class):

select * from user where age = 11 and name = 'aab' and class = '三年级一班'

按照上面的方式,每查询一次都要回表到主键索引去拿整行数据。如果改成下面这样:

select age,name,class from user where age = 11 and name = 'aab' and class = '三年级一班'

此时由于二级索引中存储了索引列的值,所以此时可以直接从索引列获取,避免了对主键索引的二次查询。

覆盖索引对 InnoDB 非常管用,而哈希索引、空间索引、全文索引都不会存储索引列的值,所以没办法使用覆盖索引。

7.使用索引扫描来做排序

如果 EXPLAIN 出来的 type 列的值为“index”,则说明使用了索引扫描来排序。

只有当索引的列顺序和 ORDER BY 子句的顺序完全一致,并且所有列的排序方向(倒序或正序)都一样时,才能使用索引来对结果做排序。如果查询关联了多张表,则只有当 ORDER BY 子句引用的字段全部为第一个表时,才能使用索引做排序。ORDER BY 子句和查询限制一样,都要求满足最左前缀原则。

但有一种情况即使不满足,也可以使用最左前缀排序。索引的第一列被指定为一个常数,此时也可以使用到索引排序。如下:

mysql> explain select age,name,class from user where age = 11 order by name,class\G;
*************************** 1. row ***************************
         type: ref
possible_keys: idx
          key: idx
          ref: const
         rows: 1
        Extra: Using where; Using index

下面是一些不能使用索引做排序的查询:

  • 使用了两种不同的排序方向

    ... where age = 11 order by name desc,class asc;
    
  • ORDER BY 子句中引用了一个不存在索引中的列

    ... where age = 11 order by name,last_name;
    
  • WHERE 和 ORDER BY 中的列无法组合成索引得最左前缀

    ... where age = 11 order by class;
    
  • 索引的第一列上是范围条件,所以后面的列无法使用索引

    ... where age > 11 order by name,class;
    
  • name 列上有多个等于条件。对于排序来说,这也是一种范围查询。但是对于where条件的查询则不算范围查询。

    ... where age = 11 and name in ('abc','bcd') order by class;
    

###四、总结

上面我们讲解了 MySQL 中的索引用法,我们平时在使用的时候要根据业务需要考虑所有列,创建合适的索引和使用合适的 sql 语句。我们要尽可能使用联合索引,并将范围查询的列放到索引靠后的位置,同时要避免多个范围条件的查询。如果能使用覆盖索引则是最好的。

参考资料:

《高性能MySQL》、丁奇老师专栏《MySQL实战45讲》