MySQL之索引详解

索引的涉及原则

索引的设计可以遵循一些已有的原则,创建索引的时候请尽量考虑符合这些原则,便于 提升索引的使用效率,更高效地使用索引。

  • 搜索的索引列,不一定是所要选择的列。换句话说,最适合索引的列是出现在 WHERE 子句中的列,或连接子句中指定的列,而不是出现在 SELECT 关键字后的选择列表中的列。
  • 使用区分度高的字段当做索引,例如性别、状态等字段就不适合添加索引。
  • 使用短索引。如果对字符串列进行索引,应该指定一个前缀长度,只要有可能就应 该这样做。例如,如果有一个 name CHAR(200)列,如果在前 10 个或 20 个字符内,多数值是惟一 的,那么就不要对整个列进行索引。对前 10 个或 20 个字符进行索引能够节省大量索引空间,(alter table tableName add key(name(10))) 也可能会使查询更快。较小的索引涉及的磁盘 IO 较少,较短的值比较起来更快。更为重要 的是,对于较短的键值,索引高速缓存中的块能容纳更多的键值,因此,MySQL 也可以在 内存中容纳更多的值。这样就增加了找到行而不用读取索引中较多块的可能性。
  • 利用最左前缀。在创建一个 n 列的索引时,实际是创建了 MySQL 可利用的 n 个索引。 多列索引可起几个索引的作用,因为可利用索引中最左边的列集来匹配行。这样的列集称为 最左前缀。
  • 不要过度索引。不要以为索引“越多越好”,什么东西都用索引是错误的。每个额 外的索引都要占用额外的磁盘空间,并降低写操作的性能。在修改表的内容时,索引必须进 行更新,有时可能需要重构,因此,索引越多,所花的时间越长。如果有一个索引很少利用 或从不使用,那么会不必要地减缓表的修改速度。此外,MySQL 在生成一个执行计划时, 要考虑各个索引,这也要花费时间。创建多余的索引给查询优化带来了更多的工作。索引太 多,也可能会使 MySQL 选择不到所要使用的最好索引。只保持所需的索引有利于查询优化。

对于 InnoDB 存储引擎的表,记录默认会按照一定的顺序保存,如果有明确定义的主 键,则按照主键顺序保存。如果没有主键,但是有唯一索引,那么就是按照唯一索引的顺序 保存。如果既没有主键又没有唯一索引,那么表中会自动生成一个内部列,按照这个列的顺 序保存。按照主键或者内部列进行的访问是最快的,所以 InnoDB 表尽量自己指定主键,当 表中同时有几个列都是唯一的,都可以作为主键的时候,要选择最常作为访问条件的列作为 主键,提高查询的效率。另外,还需要注意,InnoDB 表的普通索引都会保存主键的键值, 所以主键要尽可能选择较短的数据类型,可以有效地减少索引的磁盘占用,提高索引的缓存 效果。

索引的常见模型

哈希表

哈希示意图(图片源自MySQL实战)

哈希表是一种以键 - 值(key-value)存储数据的结构,我们只要输入待查找的键即 key,就可以找到其对应的值即 Value。哈希的思路很简单,把值放在数组里,用一个哈希函数把 key 换算成一个确定的位置,然后把 value 放在数组的这个位置。不可避免地,多个 key 值经过哈希函数的换算,会出现同一个值的情况。处理这种情况的一种方法是,拉出一个链表。

优点

1.哈希表对于单个值查询性能很快,如果没有哈希冲突复杂的为O(1),最差为O(链表的长度)。

2.新增数据时只需要在后面追加性能很高。

缺点

范围查询比较慢

有序数组

有序数组示意图(图片源自MySQL实战)

对于单个查询采用二分查找法,时间复杂度为O(logN)

优点

无论是单值查询还是范围查询性能都不错

缺点

新增的时候要保持数据有效性,如果在中间插入会导致后面所有的数据都需要移动,还会导致页分裂

搜索树

二叉搜索树示意图(图片源自MySQL实战)

二叉搜索树的特点是:父节点左子树所有结点的值小于父节点的值,右子树所有结点的值大于父节点的值。

这样如果你要查 ID_card_n2 的话,按照图中的搜索顺序就是按照 UserA -> UserC -> UserF -> User2 这个路径得到。这个时间复杂度是 O(log(N))。当然为了维持 O(log(N)) 的查询复杂度,你就需要保持这棵树是平衡二叉树。为了做这个保证,更新的时间复杂度也是 O(log(N))。树可以有二叉,也可以有多叉。多叉树就是每个节点有多个儿子,儿子之间的大小保证从左到右递增。二叉树是搜索效率最高的,但是实际上大多数的数据库存储却并不使用二叉树。其原因是,索引不止存在内存中,还要写到磁盘上。你可以想象一下一棵 100 万节点的平衡二叉树,树高 20。一次查询可能需要访问 20 个数据块。在机械硬盘时代,从磁盘随机读一个数据块需要 10 ms 左右的寻址时间。也就是说,对于一个 100 万行的表,如果使用二叉树来存储,单独访问一个行可能需要 20 个 10 ms 的时间,这个查询可真够慢的。为了让一个查询尽量少地读磁盘,就必须让查询过程访问尽量少的数据块。那么,我们就不应该使用二叉树,而是要使用“N 叉”树。这里,“N 叉”树中的“N”取决于数据块的大小。以 InnoDB 的一个整数字段索引为例,这个 N 差不多是 1200。这棵树高是 4 的时候,就可以存 1200 的 3 次方个值,这已经 17 亿了。考虑到树根的数据块总是在内存中的,一个 10 亿行的表上一个整数字段的索引,查找一个值最多只需要访问 3 次磁盘。其实,树的第二层也有很大概率在内存中,那么访问磁盘的平均次数就更少了。N 叉树由于在读写上的性能优点,以及适配磁盘的访问模式,已经被广泛应用在数据库引擎中了。

InnoDB索引模型

InnoDB采用B+树模型,所有的数据都存储在B+树上,索引分为主键索引和非主键索引。

主键索引

主键索引的叶子节点存的是整行数据。在 InnoDB 里,主键索引也被称为聚簇索引(clustered index)。

主键索引查询只需要遍历一次主键索引树就可以获取到需要的数据,因为主键索引树的叶子节点上存储了整行数据。

非主键索引

非主键索引的叶子节点内容是主键的值。在 InnoDB 里,非主键索引也被称为二级索引(secondary index)。

普通索引查询需要遍历两次索引树,第一次先遍历普通索引树找到叶子节点,普通索引树的叶子节点上只存储了主键索引,还需要再通过主键索引遍历一次主键索引树才能获取需要的数据,这个过程称为回表。

索引的维护

B+树为了维持索引的有序性,每次新增或者删除数据时需要对B+树进行重新排序。因此如果我们使用数据的自增主键每次维护索引的时候只需要在后面追加不用额外维护索引有序性,如果我们使用UUID每次插入一条数据都会对B+树进行重排序,如果某个页的数据写满了再插入数据的时候就会造成页分裂,即需要申请新的一页,然后挪动一部分数据过去。页分裂操作还影响数据页的利用率。原本放在一个页的数据,现在分到两个页中,整体空间利用率降低大约 50%。当然有分裂就有合并。当相邻两个页由于删除了数据,利用率很低之后,会将数据页做合并。合并的过程,可以认为是分裂过程的逆过程。

覆盖索引

首先创建一个表

mysql> create table T (
ID int primary key,
k int NOT NULL DEFAULT 0, 
s varchar(16) NOT NULL DEFAULT '',
index k(k))
engine=InnoDB;

insert into T values(100,1, 'aa'),(200,2,'bb'),(300,3,'cc'),(500,5,'ee'),(600,6,'ff'),(700,7,'gg');

表的索引结构如下

我们来分析一下select * from T where k between 3 and 5的执行过程

1.在 k 索引树上找到 k=3 的记录,取得 ID = 300;

2.再到 ID 索引树查到 ID=300 对应的 R3;

3.在 k 索引树取下一个值 k=5,取得 ID=500;

4.再回到 ID 索引树查到 ID=500 对应的 R4;

5.在 k 索引树取下一个值 k=6,不满足条件,循环结束。

回到主键索引树查询数据的过程称为回表。但是如果我们把sql语句改成select ID from T where k between 3 and 5,这时候就不会有回表操作了,因为在k索引树了已经有ID的值了,不要取主键索引取了,因此在适当的场景我们通过创建联合索引来实现覆盖索引减少回表也是一种SQL的手段。

唯一索引与普通索引

查询过程

例如select * from T where k=5;

  • 对于普通索引来说,查找到满足条件的第一个记录 (5,500) 后,需要查找下一个记录,直到碰到第一个不满足 k=5 条件的记录。
  • 对于唯一索引来说,由于索引定义了唯一性,查找到第一个满足条件的记录后,就会停止继续检索。

但是这两种查询方式性能差距微乎其微,因为因为引擎是按页(每个数据页大小默认16K)读写的,所以说,当找到 k=5 的记录的时候,它所在的数据页就都在内存里了。那么,对于普通索引来说,要多做的那一次“查找和判断下一条记录”的操作,就只需要一次指针寻找和一次计算,这些操作全部是基于内存的,有一种的特殊情况,就是(5,500)这条数据正好的当前页的最后一条,这时候需要将下一页的数据加载到内存中,继续查找,通常情况下每个页存有几千个key,因此这种情况的概率比较低。

更新过程

当需要更新一个数据页时,如果数据页在内存中就直接更新,而如果这个数据页还没有在内存中的话,在不影响数据一致性的前提下,InnoDB 会将这些更新操作缓存在 change buffer 中,这样就不需要从磁盘中读入这个数据页了。在下次查询需要访问这个数据页的时候,将数据页读入内存,然后执行 change buffer 中与这个页有关的操作。通过这种方式就能保证这个数据逻辑的正确性。需要说明的是,虽然名字叫作 change buffer,实际上它是可以持久化的数据。也就是说,change buffer 在内存中有拷贝,也会被写入到磁盘上。将 change buffer 中的操作应用到原数据页,得到最新结果的过程称为 merge。除了访问这个数据页会触发 merge 外,系统有后台线程会定期 merge。在数据库正常关闭(shutdown)的过程中,也会执行 merge 操作。显然,如果能够将更新操作先记录在 change buffer,减少读磁盘,语句的执行速度会得到明显的提升。而且,数据读入内存是需要占用 buffer pool 的,所以这种方式还能够避免占用内存,提高内存利用率。

对于唯一索引来说,所有的更新操作都要先判断这个操作是否违反唯一性约束。比如,要插入 (4,400) 这个记录,就要先判断现在表中是否已经存在 k=4 的记录,而这必须要将数据页读入内存才能判断。如果都已经读入到内存了,那直接更新内存会更快,就没必要使用 change buffer 了。因此,唯一索引的更新就不能使用 change buffer,实际上也只有普通索引可以使用。change buffer 用的是 buffer pool 里的内存,因此不能无限增大。change buffer 的大小,可以通过参数 innodb_change_buffer_max_size 来动态设置。这个参数设置为 50 的时候,表示 change buffer 的大小最多只能占用 buffer pool 的 50%。

现在,你已经理解了 change buffer 的机制,那么我们再一起来看看如果要在这张表中插入一个新记录 (4,400) 的话,InnoDB 的处理流程是怎样的。

第一种情况是,这个记录要更新的目标页在内存中

这时,InnoDB 的处理流程如下:对于唯一索引来说,找到 3 和 5 之间的位置,判断到没有冲突,插入这个值,语句执行结束;对于普通索引来说,找到 3 和 5 之间的位置,插入这个值,语句执行结束。这样看来,普通索引和唯一索引对更新语句性能影响的差别,只是一个判断,只会耗费微小的 CPU 时间。但,这不是我们关注的重点。

第二种情况是,这个记录要更新的目标页不在内存中。

这时,InnoDB 的处理流程如下:对于唯一索引来说,需要将数据页读入内存,判断到没有冲突,插入这个值,语句执行结束;对于普通索引来说,则是将更新记录在 change buffer,语句执行就结束了。将数据从磁盘读入内存涉及随机 IO 的访问,是数据库里面成本最高的操作之一。change buffer 因为减少了随机磁盘访问,所以对更新性能的提升是会很明显的。

之前有个 DBA 的同学反馈说,他负责的某个业务的库内存命中率突然从 99% 降低到了 75%,整个系统处于阻塞状态,更新语句全部堵住。而探究其原因后,我发现这个业务有大量插入数据的操作,而他在前一天把其中的某个普通索引改成了唯一索引。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值