MySQL图解索引篇
二、索引篇
索引常见面试题
什么是索引?
索引是数据的目录。
下图是 MySQL 的结构图,索引和数据就是位于存储引擎中:
按数据结构分类
从数据结构的角度来看,MySQL常见索引有B+ Tree索引,HASH索引,Full-Text索引。
InnoDB是在MySQL 5.5之后成为默认的MySQL存储引擎。B+Tree索引类型也是MySQL存储引擎采用最多的索引类型。
在创建表时,InnoDB存储引擎会根据不同的场景选择不同的列作为索引:
- 如果有主键,默认会使用主键作为聚簇索引的索引键(key);
- 如果没有主键,就选择第一个不包含NULL值的唯一列作为聚簇索引的索引键(key);
- 在上面两个都没有的情况下,InnoDB将自动生成一个隐式自增iD列作为聚簇索引的索引键(key)。
其他索引都属于辅助索引,也被称为二级索引或非聚簇索引。创建的主键索引和二级索引默认使用的是B+ Tree索引。
B+Tree存储千万级的数据只需要3-4层高度就可以满足,这意味着从千万级的表查询目标数据最多需要3-4次磁盘I/O,所以B+Tree相比于B树和二叉树来说,最大的优势在于查询效率很高,因为即使在数据量很大的情况,查询一个数据的磁盘1/O依然维持在3-4次。
在二级索引的B+ Tree就能查询到结果的过程就叫作「覆盖索引」,也就是只需要查一个B+ Tree就能找到数据。
按物理存储分类
从数据存的角度来看,索引分为聚簇索引(主键索引)、二级索引(辅助索引)。
这两个区分:
- 主键索引的B+ Tree的叶子节点存放的是实际数据,所有完整的用户记录都存放在主键索引的B+ Tree的叶子节点上。
- 二级索引的B+ Tree的叶子节点存放的是主键值,而不是实际数据。
所以,在查询时使用了二级索引1,如果查询的数据能在二级索引里查询的到,那么就不需要回表,这个过程就是覆盖索引l。如果查询的数据不在二级索引里,就会先检索二级索引,找到对应的叶子节点,获取到主键值后,然后再检索主键索引,就能查询到数据了,这个过程就是回表。
按字段特性分类
从字段特性的角度来看,索引分为主键索引、唯一索引、普通索引、前缀索引。
主键索引:主键索引是建立在主键字段上的索引,通过在创建表的时候(PRIMARY KEY)一起创建,一张表最多只有一个主键索引,索引列的值不允许有空值。
CREATE TABLE table_name (......PRIMARY KEY(index_column_1))USING BTREE
);
唯一索引:唯一索引建立在UNIQUE字段上的索引,一张表可以有多个唯一索引,索引列的值必须唯一,但是允许有空值。
CREATE TABLE table_name (......UNIQUE KEY(index_column_1,index_column_2,...);
);
建表后,如果要创建唯一索引,可以使用这条命令:
CREATE UNIQUE INDEX index_name
ON table_name(idx_column_1, index_column_2, ...);
普通索引:
普通索引就是建立在普通字段上的索引,既不要求字段为主键,也不要求字段为UNIQUE。
在创建表时,创建普通索引的方式如下:
CREATE TABLE table_name (....INDEX(index_column_1,index_column_2,...);
);
建表后,如果要创建普通索引,可以:
CREATE INDEX index_name
ON table_name(index_column_1,index_column_2,...);
前缀索引:前缀索引是指对字符类型字段的前几个字符建立的索引,而不是在整个字符上建立的索引,前缀索引可以建立在字段类型为char、varchar、binary、varbinary的列上。
使用前缀索引的目的是为了减少索引占用的存储空间,提升查询效率。
在创建表时,创建前缀索引的方式如下:
CREATE TABLE table_name(column_list,INDEX(column_name(length))
);
建表后,如果要创建前缀索引,可以使用命令:
CREATE INDEX index_name
ON table_name(column_name(length));
按字段个数分类
从字段个数的角度来看,索引分为单列索引、联合索引(复合索引).
- 建立在单列上的索引称为单列索引,比如主键索引;
- 建立在多列上的索引称为联合索引。
联合索引:通过将多个字段组合成一个索引,该索引就称为联合索引。
比如,将商品表中的product_no和name字段组合成联合索引(product_no, name),创建联合索引的方式如下:
CAEATE INDEX index_product_no_name ON product(product_no, name);
联合索引的非叶子节点用两个字段的值作为B+Tree的key值。当在联合索引查询数据时,先按product_no字段比较,在product_no相同的情况下再按name字段比较。
也就是说,使用联合索引是时,存在最左匹配原则,也就是按照最左优先的方式进行索引的匹配。在使用联合索引进行查询的时候,如果不遵循「最左匹配原则」,联合索引就会失效,这样就无法利用到索引快速查询的特性了。
联合索引范围查询
并不是查询过程使用了联合索引查询,就代表联合索引中的所有字段都用到了联合索引进行索引查询,也就是可能存在部分字段用到联合索引的B+Tree,部分字段没有用到联合索引的B+Tree的情况。
这种特殊情况就发生在范围查询,联合索引的最左匹配原则会一直向右匹配知道遇到「范围查询」就会停止匹配。也就是范围查询的字段可以用到联合索引,但是在范围查询字段的后面的字段无法用到联合索引。
联合索引的最左匹配原则,在遇到范围查询(如> 、<)的时候,就会停止匹配,也就是范围查询的字段可以用到联合索引,但是在范围查询字段的后面的字段无法用到联合索引。注意,对于>=、<=、BETWEEN、like前缀匹配的范围查询,并不会停止匹配。
索引下推
现在我们知道,对于联合索引l(a,b),在执行select* from table where a >1 and b = 2语句的时候,只有a字段能用到索引l,那在联合索引l的B+Tree找到第一个满足条件的主键值(ID为2)后,还需要判断其他条件是否满足(看b是否等于2),那是在联合索引里判断?还是回主键索引去判断呢?
- 在MySQL 5.6之前,只能从ID2(主键值)开始一个个回表,到「主键索引」上找到数据行,再对比b字段值。
- 而MySQL 5.6引入的索引下推优化,可以在联合索引遍历过程中,对联合索引中包含的字段先做判断,直接过滤掉不满足条件的记录,减少回表次数。
索引区分度
实际开发过程中建立联合索引时,要把区分度大的字段排在前面,这样区分大的字段越有可能被更多的SQL使用到。
区分度就是某个字段column不同值的个数「除以」表的总行数,计算公式如下:
比如,性别的区分度就很小,不适合建立索引或不适合排在联合索引列的靠前的位置,而UUID这类字段就比较适合做索引或排在联合索引列的靠前的位置。
MySQL还有一个查询优化器,查询优化器发现某个值出现在表的数据行中的百分比(惯用的百分比界线是“30%”)很高的时候,它一般会忽略索引,进行全表扫描。
联合索引进行排序(???)
这里出一个题目,针对下面这条SQL,你怎么通过索引来提高查询效率呢?
select * from order where status = 1 order by create_time asc
但是更好的方式给 status 和 create_time 列建立一个联合索引l,因为这样可以避免 MySQL数据库发生文件排序。
因为在查询时,如果只用到 status 的索引l,但是这条语句还要对create_time 排序,这时就要用文件排序filesort,也就是在 SQL 执行计划中,Extra 列会出现 Using filesort。所以,要利用索引l的有序性,在status和create_time列建立联合索引,这样根据status 筛选后的数据就
是按照create_time 排好序的,避免在文件排序,提高了查询效率。
什么时候需要/不需要创建索引?
索引的好处:提高查询速度;
索引的缺点:
- 需要占用物理空间,数量越大,占用空间越大;
- 创建索引和维护索引要耗费时间,这种时间随着数据量的增加而增大;
- 会降低表的增删改的效率,因为每次增删改索引,B+树为了维护索引有序性,都需要进行动态维护。
什么时候适用索引?
- 字段有唯一性限制的,比如商品编码;
- 经常用于WHERE查询条件的字段;
- 经常用于GROUP BY和ORDER BY的字段。
什么时候不需要创建索引?
- WHERE条件,GROUP BY,ORDER BY里用不到的字段;
- 字段中存在大量重复数据,不需要创建索引;
- 表数据太少的时候,不需要创建索引;
- 经常更新的字段不用创建索引。
有什么优化索引的方式?
常见优化索引的方法:
- 前缀索引优化:使用某个字段中字符串的前几个字符建立索引。
使用前缀索引是为了减少索引字段大小,可以增加一个索引页中存储的索引值,有效提高索引的查询速度。在一些大字符串的字作为索引时,使用前缀索引可以帮助我们减少索引项的大小。
前缀索引的局限性:- order by就无法使用前缀索引;
- 无法把前缀索引用作覆盖索引。
- 覆盖索引优化:覆盖索引是指SQL中query的所有字段,在索引B+ Tree的叶子节点上都能找得到的那些索引,从二级索引中查询得到记录,而不需要通过聚簇索引查询获得,可以避免回表的操作。使用覆盖索引的好处就是,不需要查询出包含整行记录的所有信息,也就减少了大量的I/O操作。
- 主键索引最好是自增的:InnoDB创建主键索引默认为聚簇索引l,数据被存放在了B+Tree的叶子节点上。也就是说,同一个叶子节
点内的各个数据是按主键顺序存放的,因此,每当有一条新的数据插入时,数据库会根据主键将其插入到对应的叶子节点中。- 使用自增主键,插入一条新纪录,都是追加操作,不需要重新移动数据。使用非自增主键,会导致页分裂,还可能造成大量的内存碎片,导致索引结构不紧凑,从而影响重新效率。
- 防止索引失效:用上了索引并不意味着查询的时候会使用到索引l,所以我们心里要清楚有哪些情况会导致索引失效,从而避免写出索引失效的查询语句,否则这样的查询效率是很低的。(常见面试题:说实话你知道哪些情况下会发生索引失效)
索引最好设置为NOT NULL
- 索引列存在NULL就会导致优化器在做索引选择的时候更加复杂,更加难以优化,因为可为NULL的列会使索引、索引统计和值比较都更复杂,比如进行索引统计时,count会省略为NULL的行。
- NULL值是一个没意义的值,会占用物理空间,行格式中至少会用1字节空间存储NULL值列表。
总结
为什么MySQL InnoDB选择B+tree作为索引的数据结构?
B+Tree vs B Tree:
- 存储相同数据量级别的情况下,B+Tree树高比B Tree低,磁盘I/O次数更少。
- B+Tree叶子节点用双向链表串起来,适合范围查询,B Tree无法做到这点。
B+Tree vs 二叉树:
- 随着数据量的增加,二叉树的树高会越来越高,磁盘I/O次数也会更多,B+Tree在千万级别的数据下,高度依然维持在3~4次层左右,也就是说一次数据查询操作只需要做3~4次的磁盘I/O操作就能查询到目标数据。
B+ Tree vs Hash:
- 虽然Hash的等值查询效率很高,但是无法做范围查询
什么时候适用索引?
- 字段有唯一性限制的,比如商品编码;
- 经常用于WHERE查询条件的字段;
- 经常用于GROUP BY和ORDER BY的字段。
什么时候不需要创建索引?
- WHERE条件,GROUP BY,ORDER BY里用不到的字段;
- 字段中存在大量重复数据,不需要创建索引;
- 表数据太少的时候,不需要创建索引;
- 经常更新的字段不用创建索引。
什么时候索引会失效?
- 当我们适用左或者左右模糊匹配的时候,也就是
like %xx
或者like %xx%
这两种方式都会造成索引失效; - 当我们在查询条件中对索引列做了计算、函数、类型转换操作,会导致索引失败;
- 联合索引要能正确适用需要遵循最左匹配原则,也就是按照最左优先的方式进行索引的匹配,否则就会导致索引失效;
- 在WHERE子句中,如果在OR前的条件列是索引列,而在OR后的条件列不是索引列,那么索引就会失效;
- 为了更好的利用,索引列要设置为NOT NULL约束。
有什么优化索引的方法?
- 前缀索引优化‘;
- 覆盖索引优化;
- 主键索引最好是自增的;
- 防止索引失效。