MySQL索引

2022/1/21

# MySQL索引的18个问题

# 1 索引是什么?

索引是一种数据结构,协助快速查询和更新数据库表中的数据。

索引也是一种特殊的文件,包含数据库表里所有记录的引用指针。

可以类比字典,有拼音或者笔画的快速检索,找到对应的页码,打开后即可知道某一个key的全部值信息。

# 2 索引的优缺点?

优点:

  • 大大加快检索速度。(创建索引最主要原因)
  • 使用索引,在查询过程中使用优化隐藏器,提高系统性能。

缺点:

  • 时间方面:创建索引和维护索引需要耗费时间,对索引数据增删改、索引也要维护,降低增删改效率。
  • 空间方面:索引占用物理空间。

# 3 MySQL的索引类型

存储结构划分:BTree索引(B-Tree或B+Tree索引),Hash索引,full-index全文索引,R-Tree索引。

应用层次来分:

  • 普通索引:即一个索引只包含单个列,一个表可以有多个单列索引
  • 唯一索引:索引列的值必须唯一,但允许有空值
  • 复合索引:多列值组成一个索引,专门用于组合搜索,其效率大于索引合并
  • 聚簇索引和非聚簇索引(下边介绍)

根据数据的物理顺序与键值的逻辑(索引)顺序关系: 聚集索引,非聚集索引

# 4 Mysql 索引底层数据结构选型(为什么索引结构默认使用B+Tree,而不是B-Tree,Hash,二叉树,红黑树?)

数据结构选型以如下图的user表进行分析:

user表部分数据示例

# 哈希表

哈希表可以进行数据的快速检索。

哈希算法:也叫散列算法,就是把任意值(key)通过哈希函数变换为固定长度的 key 地址,通过这个地址进行数据查询的数据结构。

哈希表原理

如果需要检索id=7的数据,sql如下:

select *
from user
where id = 7;
1
2
3

哈希算法快速检索数据的计算过程:首先计算存储 id=7 的数据的物理地址 addr=hash(7)=4231,而 4231 映射的物理地址是 0x77,0x77 就是 id=7 存储的额数据的物理地址,通过该独立地址可以找到对应 user_name='g'这个数据。

但是hash算法可能出现碰撞问题,即hash函数可能计算相同的key值,不同的key映射到了同一个结果。解决碰撞问题的常见方法是链地址法 :碰撞数据使用链表连接,计算hash值后,判断该值如果有碰撞,遍历到链表,直到找到真正的key所对应的数据为止。

hash碰撞-链地址法

从算法时间复杂度分析来看,哈希算法时间复杂度为 O(1),检索速度非常快。比如查找 id=7 的数据,哈希索引只需要计算一次就可以获取到对应的数据,检索速度非常快。但是 Mysql 并没有采取哈希作为其底层算法。

select * from user where id >3;
1

即hash算法针对范围查找效率太低,需要把所有数据找出来加载到内存,然后再在内存里筛选筛选目标范围内的数据。

总结:hash表可以快速检索数据,但是范围查找效率低。所以不适合作为MySQL的索引数据结构

# 二叉查找数(BST)

二叉查找树支持快速查找数据,时间复杂度为O(logn)。如下图所示,只需计算三次即可找到id=7的数据。需要考虑其是否能解决hash表范围查找效率低的问题。

image-20210626101831123

二叉查找数按序排列(从左到右升序),如果想找到id>5的数据,只需要找到节点6及其右子树的数据即可,范围查找比较容易实现。

普通二叉树的致命缺点:极端情况下会退化为线性链表,时间复杂度为O(n),性能急剧下降,如下图:

二叉树退化为线性链表

数据库表的主键id一般为自增,上述的线性结构查找问题必然出现,而且频率还很高。

总结:二叉查找数查询效率高,而且可以解决hash表范围查询效率低的问题,但是会频繁出现不平衡退化问题导致查询效率低的问题,所以不适合作为MySQL索引的数据结构

# 红黑树

二叉查找树不平衡,可以通过树节点的自动旋转和调整来解决,从而保证二叉树的查找性能。最常见的思路是平衡二叉树和红黑树。

红黑树可以自动调整树的结构,当二叉树不平衡时,红黑树自动左右旋转和节点变色,保持基本平衡,时间复杂度为O(logn),查询效率不会降低。如下图,红黑树查找id=7的节点只需要查找4次:

红黑树查找节点

但是红黑树也有缺点,即“右倾”现象(参看下图),虽然没有二叉树退化那么夸张,但数据库主键基本都是自增,面对成千上百万的数据,这查询效率可想而知。

红黑树“右倾”现象

总结:红黑树查询效率与二叉查找树相似,极端退化情况比平衡二叉树好,但是也没能达到预期,所以不适合MySQL索引的数据结构

# 平衡二叉树(AVL)

平衡二叉树,是绝对平衡的。

AVL 树顺序插入 1~7 个节点,查找 id=7 所要比较节点的次数为 3。

AVL 树插入 1~16 个节点,查找 id=16 需要比较的节点数为 4。从查找效率而言,AVL 树查找的速度要高于红黑树的查找效率(AVL 树是 4 次比较,红黑树是 6 次比较)。

从树的形态看来,AVL 树不存在红黑树的“右倾”问题。大量的顺序插入不会导致查询性能的降低,这从根本上解决了红黑树的问题。

AVL 树的优点

  • 查询效率高O(logn),不存在极端情况。
  • 可以进行范围查找和排序。

但是为什么不选取AVL树作为MySQL索引的数据结构

主要是磁盘IO因素的影响。如果使用AVL树,每一个树节点,只存储一个数据。如果查询id=7的数据,需要比对三次树节点,即进行三次磁盘IO操作,如果数据量大了,那磁盘IO的次数会很高,消耗大量的时间。

所以,设计数据库索引的时候,还需要考虑怎么尽可能减少磁盘的IO次数

磁盘读取1B和1KB的数据消耗的时间基本是一样的,所以可以在一个节点存储更多的数据,即一次磁盘IO读取更多数据,即可解决问题。所以就考虑到了B树

# B树(B-树)

B树的理解参考:平衡二叉树、B树、B+树、B*树 理解其中一种你就都明白了 (opens new window)

B树,平衡多路查找树,又称B-树。

如果每个节点限制最多存储两个key(即二叉树),一个节点如果超过两个key会自动分裂。

比如下面这个存储了 7 个数据 B 树,只需要查询两个节点就可以知道 id=7 这数据的具体位置,也就是两次磁盘 IO 就可以查询到指定数据,优于 AVL 树。

B树7节点(限制单节点key=2)

如果是一个存储了 16 个数据的 B 树,同样每个节点最多存储 2 个 key,查询 id=16 这个数据需要查询比较 4 个节点,也就是经过 4 次磁盘 IO。看起来查询性能与 AVL 树一样。

B树16节点(限制单节点key=2)

如果限制每个节点可以存储6个key。

一个存储了 7 个数据的 B 树,查询 id=7 这个数据所要进行的磁盘 IO 为 2 次。

B树7节点(限制单节点key=6)

一个存储了 16 个数据的 B 树,查询 id=7 这个数据所要进行的磁盘 IO 为 2 次。相对于 AVL 树而言磁盘 IO 次数降低为一半。

B树16节点(限制单节点key=6)

B 树作数据库索引优点

  • 优秀检索速度,时间复杂度:B 树的查找性能等于 O(h*logn),其中 h 为树高,n 为每个节点关键词的个数;
  • 尽可能少的磁盘 IO,加快了检索速度;
  • 可以支持范围查找。

B-Tree能加快数据的访问速度,因为存储引擎不再需要进行全表扫描来获取数据,但是数据分布在各个节点之中,每个节点存储的数据量是有限的,MySQL希望一个节点可以尽可能多的存储数据,因此采用了B+树。

# B+树

  • B树一个节点存储的是数据,一个节点中存储不了太多数据;而B+树非叶子节点存储的是地址,叶子结点存储的是数据,可以存储更多数据。
  • B+数叶子结点采用链表串联,更便于范围查找。而B树需要中序遍历。

B+树

# 5 Innodb 引擎和 Myisam 引擎对索引的实现

Myisam 虽然数据查找性能极佳,但是不支持事务处理。Innodb 最大的特色就是支持了 ACID 兼容的事务功能,而且他支持行级锁。Mysql 建立表的时候就可以指定引擎。B+树作为 Mysql 的索引的数据结构非常合适,那么两种引擎是怎么实现的呢?

在执行建表语句并指定引擎后,Innodb 生成的文件有:

  • frm:创建表的语句
  • idb:表里面的数据+索引文件

Myisam 生成的文件有:

  • frm:创建表的语句
  • MYD:表里面的数据文件(myisam data)
  • MYI:表里面的索引文件(myisam index)

从生成的文件看来,这两个引擎底层数据和索引的组织方式并不一样,MyISAM 引擎把数据和索引分开了,一人一个文件,这叫做非聚集索引方式;Innodb 引擎把数据和索引放在同一个文件里了,这叫做聚集索引方式

接下来从底层实现的角度分析。

MyISAM 引擎的底层实现(非聚集索引方式)

MyISAM 用的是非聚集索引方式,即数据和索引落在不同的两个文件上。MyISAM 在建表时以主键作为 KEY 来建立主索引 B+树,树的叶子节点存的是对应数据的物理地址通过这个物理地址后,就可以到 MyISAM 数据文件中直接定位到具体的数据记录了

MyISAM 引擎的底层实现

在为其他字段添加索引时,同样会生成对应的索引树,检索方式与上述相同。

Innodb 引擎的底层实现(聚集索引方式)

InnoDB 是的主键索引是聚集索引方式,数据和索引都存储在同一个文件里。首先 InnoDB 会根据主键 ID 作为 KEY 建立索引 B+树,而 B+树的叶子节点存储的是主键 ID 对应的数据。在根据主键ID查询时,会查询这颗主键ID的索引树,找到对应叶子结点的数据。

建表的时候,InnoDB就会建好主键ID的索引树,这也是为什么 Mysql 在建表时要求必须指定主键的原因。

在为其他字段建立索引时,非叶子结点存储当前字段的key,叶子结点存储主键的key。得到主键key后,才会在主键索引树中找到当前字段所对应的数据。

Innodb 引擎的底层实现

为什么 InnoDB 只在主键索引树的叶子节点存储了具体数据,但是其他索引树却不存具体数据呢,而要多此一举先找到主键,再在主键索引树找到对应的数据呢

因为 InnoDB 要节省存储空间。一个表里可能有很多个索引,如果给每个加了索引的字段生成索引树,都存储了具体数据,那么这个表的索引数据文件就变得非常巨大(数据极度冗余了)。从节约磁盘空间的角度来说,没有必要,通过这种看似“多此一举”的步骤,在牺牲较少查询的性能下节省了巨大的磁盘空间。

为什么InnoDB 和MyISAM 对比,MyISAM 查询性能更好?

从上面索引文件数据文件的设计来看也可以看出原因:

  • MyISAM 直接找到物理地址后就可以直接定位到数据记录
  • InnoDB 查询到叶子节点后,还需要再查询一次主键索引树,才可以定位到具体数据

等于 MyISAM 一步就查到了数据,但是 InnoDB 要两步,所以 MyISAM 查询性能更高。

# 6 InnoDB中一棵B+树能存多少行数据?

约 2 千万

参看:

# 7 聚簇索引和非聚簇索引

聚簇索引是叶子结点存储整行数据,即数据与索引存储在一块,找到索引也就找到了数据。InnoDB的主键索引是聚簇索引。

非聚簇索引是叶子结点存储了主键的值,也被称为二级索引。

  • 非聚集索引与聚集索引的区别在于非聚集索引的叶子节点不存储表中的数据,而是存储该列对应的主键
  • 对于InnoDB来说,非主键的索引查到了主键的值,还需要去主键索引树再次查找数据,称这个过程为回表
  • 通常情况下, 主键索引(聚簇索引)查询只会查一次,而非主键索引(非聚簇索引)需要回表查询多次。
  • MyISAM无论主键索引还是二级索引都是非聚簇索引,而InnoDB的主键索引是聚簇索引,二级索引是非聚簇索引。我们自己建的索引基本都是非聚簇索引。

# 8 非聚簇索引一定会回表查询吗?(覆盖索引)

不一定,这涉及到查询语句所要求的字段是否全部命中了索引,如果全部命中了索引,那么就不必再进行回表查询。一个索引包含(覆盖)所有需要查询字段的值,被称之为"覆盖索引"。

例子:假设在学生表的成绩(score)上建立了索引,那么当进行select score from student where score > 90的查询时,在索引的叶子节点上,已经包含了score 信息,不会再次进行回表查询。

# 9 联合索引是什么?为什么需要注意联合索引中的顺序?

联合索引:使用多个字段同时建立一个索引。

在联合索引中,只有按照建立索引时的字段顺序使用,才能命中。

例子:假设建立“name,age,school”的联合索引,那么索引的排序为: 先按照name排序进行索引,如果name相同,则按照age排序索引,如果age的值也相等,则按照school进行排序索引。

因此在建立联合索引的时候应该注意索引列的顺序,一般情况下,将查询需求频繁或者字段选择性高的列放在前面。

# 10 MySQL的最左前缀原则?

最左前缀原则就是最左优先,在创建多列索引时,要根据业务需求,where子句中使用最频繁的一列放在最左边。 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的顺序可以任意调整。

=in可以乱序,比如a = 1 and b = 2 and c = 3 建立(a,b,c)索引可以任意顺序,mysql的查询优化器会优化成索引可以识别的形式。

# 11 前缀索引?

可能出现建立索引字段非常长的情况,这样既占用内存空间,也不利于维护。所以可以选取字段前面的公共部分作为一个索引,大大提升检索效率。但是ORDER BYGROUP BY不支持前缀索引 。

流程:

  1. 计算完整列的选择性 :select count(distinct col_1)/count(1) from table_1
  2. 计算不同前缀长度的选择性 :select count(distinct left(col_1,4))/count(1) from table_1
  3. 找到最优长度之后,创建前缀索引 :create index idx_front on table_1 (col_1(4))

注意事项:

  • 前缀索引是一种能使索引更小,更快的有效办法,但另一方面也有其缺点:mysql无法使用其前缀索引做ORDER BY和GROUP BY,也无法使用前缀索引做覆盖扫描
  • 要明确使用前缀索引的目的与优势
    • 大大节约索引空间,从而提高索引效率
    • 对于 BOLB 、 TEXT 或者很长的 VARCHAR 类型的列,必须使用前缀索引,因为 MySQL 不允许索引这些列的完整长度
  • 前缀索引会降低索引的选择性
    • 关于索引的选择性,它是指不重复的索引值(也称为基数cardinality)和数据表的记录总数的比值,范围从1/(数据表记录总数) 到1之间。索引的选择性越高则查询效率越高,因为选择性高的索引可以让MySQL在查找时过滤掉更多的行。选择性为1的索引叫唯一索引,这是最好的索引选择性,性能也是最好的。
  • 真正的难点在于:要选择足够长的前缀以保证较高的选择性,同时又不能太长, 前缀的长度应该使前缀索引的选择性接近索引整个列,即前缀的基数应该接近于完整列的基数

前缀索引分析可参看:MySQL 前缀索引 (opens new window)

# 12 索引下推?

MySQL 5.6引入了索引下推优化。默认开启,使用SET optimizer_switch = ' index_condition_pushdown=off ';可以将其关闭。有了索引下推优化,可以在减少回表次数

官方解释:

在 people_table中有一个二级索引(zipcode,lastname,firstname) ,查询语句:SELECT * FROM people WHERE zipcode='95054' AND lastname LIKE '%etrunia%' AND address LIKE '%Main Street%' ;

如果没有使用索引下推技术,则MySQL会通过zipcode='95054'从存储引擎中查询对应的数据,返回到MySQL服务端(回表 ),然后MySQL服务端基于lastname LIKE '%etrunia%' AND address LIKE '%Main Street%'来判断数据是否符合条件。

如果使用了索引下推技术,则MYSQL首先会返回符合zipcode='95054'的索引,然后根据lastname LIKE '%etrunia%' AND address LIKE '%Main Street%' 来判断索引是否符合条件。如果符合条件,则根据该索引来定位对应的数据,如果不符合,则直接reject掉。

注意:在InnoDB中索引下推只对二级索引有效

# 13 怎么查看MySQL语句有没有用到索引?

可以通过explain查看sql语句的执行计划,通过执行计划来分析索引使用情况,只需要将explain添加在sql语句之前即可。

表中的索引:

表中的索引

通过explain查看sql是否用到索引:

explain查看sql的结果

  • type 的信息很明显的体现是否用到索引,它提供了判断查询是否高效的重要依据依据,如const(主键索引或者唯一二级索引进行等值匹配的情况下),ref(普通的⼆级索引列与常量进⾏等值匹配),index(扫描全表索引的覆盖索引) 。性能如下:ALL < index < range ~ index_merge < ref < eq_ref < const < systemALL 类型因为是全表扫描, 因此在相同的查询条件下, 它是速度最慢的. 而 index 类型的查询虽然不是全表扫描, 但是它扫描了所有的索引, 因此比 ALL 类型的稍快。
  • select_type:select关键字对应的那个查询的类型,如SIMPLE,PRIMARY,SUBQUERY,DEPENDENT,SNION 。
  • table:每个查询对应的表名 。
  • possible_key:查询中可能用到的索引
  • key:此字段是 MySQL 在当前查询时所真正使用到的索引。
  • filtered:查询器预测满足下一次查询条件的百分比 。
  • rows: 显示MySQL认为它执行查询时必须检查的行数。这个值非常直观显示 SQL 的效率好坏, 原则上 rows 越少越好。
  • extra:表示额外信息。

# 14 什么情况下不走索引(索引失效)?

  1. 使用!= 或者 <> 导致索引失效
  2. 类型不一致导致的索引失效
  3. 函数导致的索引失效。如:SELECT * FROM user WHERE DATE(create_time) = '2020-09-03'; 如果create_time添加了索引,索引会失效。
  4. 运算符导致的索引失效。如:SELECT * FROM user WHERE age - 1 = 20; 如果对列进行了(+,-,*,/,!), 那么都将不会走索引。
  5. OR引起的索引失效。OR导致索引是在特定情况下的,并不是所有的OR都是使索引失效,如果OR连接的是同一个字段,那么索引不会失效,反之OR右侧字段索引失效。
  6. 模糊搜索导致的索引失效。当%放在匹配字段前是不走索引的,放在后面才会走索引。
  7. NOT INNOT EXISTS导致索引失效

# 15 为什么官方建议使用自增长数字主键作为索引?

建议使用有序的自增ID作为主键

提高效率主要体现在:

  • 提高范围查询效率;
  • 增加排序效率;
  • 提高扫表能力,顺序访问。

结合B+树的特点,一个表有多少个索引就会有多少颗B+树,MySQL的数据都是按顺序保存在树的叶子结点上的。

mysql 在底层又是以数据页为单位来存储数据的,一个数据页大小默认为 16k(可以自定义大小)。如果一个数据页存满了,mysql 就会去申请一个新的数据页来存储数据。

  • 如果主键为自增 id 的话,mysql 在写满一个数据页的时候,直接申请另一个新数据页接着写就可以了。
  • 如果主键是非自增 id,为了确保索引有序,mysql 就需要将每次插入的数据都放到合适的位置上。当往一个快满或已满的数据页中插入数据时,新插入的数据会将数据页写满,mysql 就需要申请新的数据页,并且把上个数据页中的部分数据挪到新的数据页上。这就造成了页分裂,这个大量移动数据的过程是会严重影响插入效率的。

InnoDB逻辑结构

在满足业务需求的情况下,尽量使用占空间更小的主键

  • 主键占用空间越大,每个页存储的主键个数越少,路树就越少,B+树的深度会边长,导致IO次数会变多。
  • 普通索引的叶子节点上保存的是主键 id 的值,如果主键 id 占空间较大的话,那将会成倍增加 mysql 空间占用大小。

索引流程图

总结:使用自增主键可以提高效率(范围查询、排序、扫表),而且自增数字占用更小的空间,可以存储更多的数据。

# 16 如何创建索引?

1、在执行CREATE TABLE时创建索引

CREATE TABLE user_index2
(
    id          INT auto_increment PRIMARY KEY,
    name        VARCHAR(16),
    id_card     VARCHAR(18),
    information text,
    INDEX (id_card)
);
1
2
3
4
5
6
7
8

2、使用ALTER TABLE命令去增加索引

ALTER TABLE table_name
    ADD INDEX index_name (column_list);
1
2

ALTER TABLE用来创建普通索引、UNIQUE索引或PRIMARY KEY索引。

# 17 创建索引时需要注意什么?建索引的原则有哪些?

注意事项:

  • 较频繁的作为查询条件的字段应该创建索引
  • 唯一性太差的字段不适合单独创建索引,即使该字段频繁作为查询条件。
  • 更新非常频繁的字段不适合创建索引
  • 非空字段:应该指定列为NOT NULL,除非你想存储NULL。在mysql中,含有空值的列很难进行查询优化,因为它们使得索引、索引的统计信息以及比较运算更加复杂。你应该用0、一个特殊的值或者一个空串代替空值。
  • 取值离散大的字段(变量各个取值之间的差异程度),将其列放到联合索引的前面,可以通过count()函数查看字段的差异值,返回值越大说明字段的唯一值越多字段的离散程度高。
  • 索引字段越小越好:数据库的数据存储以页为单位一页存储的数据越多一次IO操作获取的数据越大效率越高。

原则:

  • 最左前缀匹配原则。在创建多列索引时,要根据业务需求,where子句中使用最频繁的一列放在最左边。
  • =in可以乱序。比如a = 1 and b = 2 and c = 3 建立(a,b,c)索引可以任意顺序,mysql的查询优化器会帮你优化成索引可以识别的形式。
  • 尽量选择区分度高的列作为索引。区分度的公式是count(distinct col)/count(*),表示字段不重复的比例,比例越大我们扫描的记录数越少,唯一键的区分度是1。
  • 索引列不能参与计算。计算代表逻辑计算和使用函数,会使索引失效。
  • 尽量的扩展索引,不要新建索引。比如表中已经有a的索引,现在要加(a,b)的索引,那么只需要修改原来的索引即可。

# 18 使用索引查询一定能提高查询的性能吗?

使用索引查询不一定能提高查询性能.

通常通过索引查询数据比全表扫描要快,但是我们也必须注意到使用的代价

索引需要空间来存储,也需要定期维护, 每当有记录在表中增减或索引列被修改时,索引本身也会被修改。

这意味着每条记录的INSERTDELETEUPDATE将为此多付出4,5 次的磁盘I/O。 因为索引需要额外的存储空间和处理,那些不必要的索引反而会使查询反应时间变慢。

索引范围查询(INDEX RANGE SCAN)适用于两种情况:

  1. 基于一个范围的检索,一般查询返回结果集小于表中记录数的30%。

  2. 基于非唯一性索引的检索。

否则索引范围查询的效率会大大降低。

# 19. mysql索引对Null值如何处理的?

  • 只要列中包含有NULL值都将不会被包含在索引中
  • 复合索引中只要有一列含有NULL值,那么这一列对于此复合索引就是无效的。

所以我们在数据库设计时不要让字段的默认值为NULL。

# 参考

# 参考