MySQL:索引常见问题
MySQL:索引常见问题
1. 什么是索引
索引的定义就是帮助存储引擎快速获取数据的一种数据结构,形象的说就是索引是数据的目录。
2. 索引分类
- 按「数据结构」分类:B+tree索引、Hash索引、Full-text索引。
- 按「物理存储」分类:聚簇索引(主键索引)、二级索引(辅助索引)。
- 按「字段特性」分类:主键索引、唯一索引、普通索引、前缀索引。
- 按「字段个数」分类:单列索引、联合索引。
2.1 按数据结构分类
2.1.1 索引的选择
在创建表时,InnoDB 存储引擎会根据不同的场景选择不同的列作为索引:
- 如果有主键,默认会使用主键作为聚簇索引的索引键(key);
- 如果没有主键,就选择第一个不包含 NULL 值的唯一列作为聚簇索引的索引键(key);
- 在上面两个都没有的情况下,InnoDB 将自动生成一个隐式自增 id 列作为聚簇索引的索引键(key);
其它索引都属于辅助索引(Secondary Index),也被称为二级索引或非聚簇索引。
创建的主键索引和二级索引默认使用的是 B+Tree 索引。
2.1.2 B+ Tree索引结构特点
B+Tree的结构特点如下:
- B+Tree 是一种多叉树,叶子节点才存放数据,非叶子节点只存放索引。
- 每个节点里的数据是按主键顺序存放的。
- 每一层父节点的索引值都会出现在下层子节点的索引值中。
- 每一个叶子节点都有两个指针,分别指向下一个叶子节点和上一个叶子节点,形成一个双向链表。
2.1.3 聚簇索引和非聚簇索引区别
主键索引的 B+Tree 和二级索引的 B+Tree 区别如下:
- 主键索引的 B+Tree 的叶子节点存放的是实际数据,所有完整的用户记录都存放在主键索引的 B+Tree 的叶子节点里;
- 二级索引的 B+Tree 的叶子节点存放的是主键值,而不是实际数据。
2.1.4 B+ Tree优点
- B+Tree vs B Tree
- B+Tree 只在叶子节点存储数据,而 B 树 的非叶子节点也要存储数据,所以 B+Tree 的单个节点的数据量更大,在相同的磁盘 I/O 次数下,就能查询更多的节点。
- B+Tree 叶子节点采用的是双链表连接,适合 MySQL 中常见的基于范围的顺序查找。
- B+ Tree vs 二叉树
- 对于有 N 个叶子节点的 B+Tree,其搜索复杂度为
O(logdN)
,其中 d 表示节点允许的最大子节点个数为d 个。在实际的应用当中, d 值是大于100的,这样就保证了,即使数据达到千万级别时,B+Tree 的高度依然维持在 3~4 层左右。 - 而二叉树的每个父节点的儿子节点个数只能是 2 个,意味着其搜索复杂度为
O(logN)
。
- 对于有 N 个叶子节点的 B+Tree,其搜索复杂度为
- B+ Tree vs hash
- Hash 表不适合做范围查询,它更适合做等值的查询。
2.1.5 举例说明
例如:
我新建一个表,如下图所示:
表中添加了这些数据:
那么其聚簇索引的B+ Tree应该向下面一样(这里应该是双向链表,图中是单向链表):
其二级索引的B+Tree应该向下面一样:
如果我用 product_no 二级索引查询商品,如下查询语句:
1 | select * from product where product_no = '0002'; |
会先检二级索引中的 B+Tree 的索引值(商品编码,product_no),找到对应的叶子节点,然后获取主键值,然后再通过主键索引中的 B+Tree 树查询到对应的叶子节点,然后获取整行数据。这个过程叫「回表」,也就是说要查两个 B+Tree 才能查到数据。
2.2 按物理存储分类
从物理存储的角度来看,索引分为聚簇索引(主键索引)、二级索引(辅助索引)。
这两个区别在前面也提到了:
- 主键索引的 B+Tree 的叶子节点存放的是实际数据,所有完整的用户记录都存放在主键索引的 B+Tree 的叶子节点里;
- 二级索引的 B+Tree 的叶子节点存放的是主键值,而不是实际数据。
所以,在查询时使用了二级索引,如果查询的数据能在二级索引里查询的到,那么就不需要回表,这个过程就是覆盖索引。如果查询的数据不在二级索引里,就会先检索二级索引,找到对应的叶子节点,获取到主键值后,然后再检索主键索引,就能查询到数据了,这个过程就是回表。
2.3 按字段特性分类
从字段特性的角度来看,索引分为主键索引、唯一索引、普通索引、前缀索引。
2.3.1 主键索引
主键索引就是建立在主键字段上的索引,通常在创建表的时候一起创建,一张表最多只有一个主键索引,索引列的值不允许有空值。
在创建表时,创建主键索引的方式如下:
1 | CREATE TABLE table_name ( |
2.3.2 唯一索引
唯一索引建立在 UNIQUE 字段上的索引,一张表可以有多个唯一索引,索引列的值必须唯一,但是允许有空值。
在创建表时,创建唯一索引的方式如下:
1 | CREATE TABLE table_name ( |
2.3.3 普通索引
普通索引就是建立在普通字段上的索引,既不要求字段为主键,也不要求字段为 UNIQUE。
在创建表时,创建普通索引的方式如下:
1 | CREATE TABLE table_name ( |
2.3.4 前缀索引
前缀索引是指对字符类型字段的前几个字符建立的索引,而不是在整个字段上建立的索引,前缀索引可以建立在字段类型为 char、 varchar、binary、varbinary 的列上。
使用前缀索引的目的是为了减少索引占用的存储空间,提升查询效率。
在创建表时,创建前缀索引的方式如下:
1 | CREATE TABLE table_name( |
2.4 按字段个数分类
从字段个数的角度来看,索引分为单列索引、联合索引(复合索引)。
- 建立在单列上的索引称为单列索引,比如主键索引;
- 建立在多列上的索引称为联合索引;
2.4.1 联合索引
通过将多个字段组合成一个索引,该索引就被称为联合索引。比如,将商品表中的 product_no 和 name 字段组合成联合索引(product_no, name)
,创建联合索引的方式如下:
1 | CREATE INDEX index_product_no_name ON product(product_no, name); |
联合索引(product_no, name)
的 B+Tree 示意图如下(叶子节点之间应该是双向链表):
最左匹配原则:联合索引查询的 B+Tree 是先按 product_no 进行排序,然后再 product_no 相同的情况再按 name 字段排序。
2.4.2 举例
比如,如果创建了一个 (a, b, c)
联合索引,如果查询条件是以下这几种,就可以匹配上联合索引:
- where a=1;
- where a=1 and b=2 and c=3;
- where a=1 and b=2;
需要注意的是,因为有查询优化器,所以 a 字段在 where 子句的顺序并不重要。
但是,如果查询条件是以下这几种,因为不符合最左匹配原则,所以就无法匹配上联合索引,联合索引就会失效:
- where b=2;
- where c=3;
- where b=2 and c=3;
上面这些查询条件之所以会失效,是因为(a, b, c)
联合索引,是先按 a 排序在 a 相同的情况再按 b 排序,在 b 相同的情况再按 c 排序。所以,b 和 c 是全局无序,局部相对有序的
2.4.3 联合索引的范围查询
联合索引的最左匹配原则会一直向右匹配直到遇到「范围查询」就会停止匹配。也就是范围查询的字段可以用到联合索引,但是在范围查询字段的后面的字段无法用到联合索引。
例如,select * from t_table where a > 1 and b = 2,联合索引(a, b)哪一个字段用到了联合索引的 B+Tree?
由于联合索引(二级索引)是先按照 a 字段的值排序的,所以符合 a > 1 条件的二级索引记录肯定是相邻,于是在进行索引扫描的时候,可以定位到符合 a > 1 条件的第一条记录,但是a>1时的b字段是无序的,所以这条查询语句只有 a 字段用到了联合索引进行索引查询,而 b 字段并没有使用到联合索引。
但是注意,并不是所有的范围查询都会有部分字段无法使用联合索引。
例如,select * from t_table where a >= 1 and b = 2
,联合索引(a, b)哪一个字段用到了联合索引的 B+Tree?
虽然在符合 a>= 1 条件的二级索引记录的范围里,b 字段的值是「无序」的,但是对于符合 a = 1 的二级索引记录的范围里,b 字段的值是「有序」的。 于是,在确定需要扫描的二级索引的范围时,当二级索引记录的 a 字段值为 1 时,可以通过 b = 2 条件减少需要扫描的二级索引记录范围。所以,Q2 这条查询语句 a 和 b 字段都用到了联合索引进行索引查询。
2.4.4 总结
**联合索引的最左匹配原则,在遇到范围查询(如 >、<)的时候,就会停止匹配,也就是范围查询的字段可以用到联合索引,但是在范围查询字段的后面的字段无法用到联合索引。** 注意,对于 >=、<=、BETWEEN、like 前缀匹配的范围查询,并不会停止匹配。
2.4.5 索引下推优化
现在我们知道,对于联合索引(a, b),在执行select * from table where a > 1 and b = 2
语句的时候,只有 a 字段能用到索引,那在联合索引的 B+Tree 找到第一个满足条件的主键值ID 为 2)后,还需要判断其他条件是否满足(看 b 是否等于 2),那是在联合索引里判断?还是回主键索引去判断呢?
MySQL 5.6 引入的索引下推优化(index condition pushdown), 可以在联合索引遍历过程中,对联合索引中包含的字段先做判断,直接过滤掉不满足条件的记录,减少回表次数。
2.4.6 索引区分度
实际开发工作中建立联合索引时,要把区分度大的字段排在前面,这样区分度大的字段越有可能被更多的 SQL 使用到。
区分度就是某个字段 column 不同值的个数「除以」表的总行数,计算公式如下:
$$
区分度 =\frac{\operatorname{distinct}(\operatorname{column})}{\operatorname{count}(*)}
$$
比如,性别的区分度就很小,不适合建立索引或不适合排在联合索引列的靠前的位置,而 UUID 这类字段就比较适合做索引或排在联合索引列的靠前的位置。
注意,对于 >=、<=、BETWEEN、like 前缀匹配的范围查询,并不会停止匹配
3. 什么时候需要/不需要索引
索引最大的好处是提高查询速度,但是索引也是有缺点的,比如:
- 需要占用物理空间,数量越大,占用空间越大;
- 创建索引和维护索引要耗费时间,这种时间随着数据量的增加而增大;
- 会降低表的增删改的效率,因为每次增删改索引,B+ 树为了维护索引有序性,都需要进行动态维护。
3.1 什么时候适用索引
- 字段有唯一性限制的,比如商品编码;
- 经常用于
WHERE
查询条件的字段,这样能够提高整个表的查询速度,如果查询条件不是一个字段,可以建立联合索引。 - 经常用于
GROUP BY
和ORDER BY
的字段,这样在查询的时候就不需要再去做一次排序了,因为我们都已经知道了建立索引之后在 B+Tree 中的记录都是排序好的。
3.2 什么时候不需要创建索引
WHERE
条件,GROUP BY
,ORDER BY
里用不到的字段- 字段中存在大量重复数据,不需要创建索引,比如性别字段,只有男女。
- 表数据太少的时候,不需要创建索引
- 经常更新的字段不用创建索引
3.3 优化索引的方法
3.3.1 前缀索引优化
使用某个字段中字符串的前几个字符建立索引,使用前缀索引是为了减小索引字段大小。
3.3.2 覆盖索引优化
覆盖索引是指 SQL 中 query 的所有字段,在索引 B+Tree 的叶子节点上都能找得到的那些索引,从二级索引中查询得到记录,而不需要通过聚簇索引查询获得,可以避免回表的操作。
假设我们只需要查询商品的名称、价格,有什么方式可以避免回表呢?
我们可以建立一个联合索引,即「商品ID、名称、价格」作为一个联合索引。
3.3.3 主键索引自增
如果我们使用自增主键,那么每次插入的新数据就会按顺序添加到当前索引节点的位置,不需要移动已有的数据,当页面写满,就会自动开辟一个新页面。
3.3.4 索引最好设置为NOT NULL
第一原因:索引列存在 NULL 就会导致优化器在做索引选择的时候更加复杂,更加难以优化,因为可为 NULL 的列会使索引、索引统计和值比较都更复杂,比如进行索引统计时,count 会省略值为NULL 的行。
NULL 值是一个没意义的值,但是它会占用物理空间,所以会带来的存储空间的问题,因为 InnoDB 存储记录的时候,如果表中存在允许为 NULL 的字段,那么行格式中至少会用 1 字节空间存储 NULL 值列表,如下图的紫色部分:
3.3.5 防止索引失效
之后会说