索引
索引是为了提高数据查询的效率,就像书的目录一样。
索引的常见模型
索引的出现是为了提高查询效率,但是实现索引的方式却有很多种。
1. 哈希表
哈希表是一种以键 - 值(key-value)存储数据的结构,我们只要输入待查找的值即 key,就可以找到其对应的值即 Value。哈希的思路很简单,把值放在数组里,用一个哈希函数把 key 换算
成一个确定的位置,然后把 value 放在数组的这个位置。
缺点:不是有序的,区间查询全部扫描。哈希表这种结构适用于只有等值查询的场景
多个 key 值经过哈希函数的换算,会出现同一个值的情况。处理这种情况的一种方法是,拉出一个链表。(类似hashmap)
示例:一个身份证信息和姓名的表,需要根据身份证号查找对应的名字。
假设,这时候你要查 ID_card_n2 对应的名字是什么,处理步骤就是:首先,将 ID_card_n2 通过哈希函数算出 N;然后,按顺序遍历,找到 User2。
图中四个 ID_card_n 的值并不是递增的,这样做的好处是增加新的 User 时速度会很快,只需要往后追加。但缺点是,因为不是有序的,所以哈希索引做区间查询的速度是很慢的(全部扫描)。
2. 有序数组
等值查询和范围查询场景中的性能就都很好。有序数组索引只适用于静态存储引擎。
如果仅仅看查询效率,有序数组就是最好的数据结构了。但是,在需要更新数据的时候就麻烦了,你往中间插入一个记录就必须得挪动后面所有的记录,成本太高。
假设身份证号没有重复,这个数组就是按照身份证号递增的顺序保存的。如果要查 ID_card_n2 对应的名字,用二分法就可以快速得到,这个时间复杂度是 O(log(N))。
支持范围查询。如果要查身份证号在 [ID_card_X, ID_card_Y] 区间的 User,可以先用二分法找到 ID_card_X(如果不存在 ID_card_X,就找到大于 ID_card_X 的第一个 User),然后向右遍历,
直到查到第一个大于ID_card_Y 的身份证号,退出循环。
3. InnoDB 的索引模型
在 InnoDB 中,表都是根据主键顺序以索引的形式存放的,这种存储方式的表称为索引组织表。InnoDB 使用了 B+ 树索引模型,所以数据都是存储在 B+ 树中的。
每一个索引在 InnoDB 里面对应一棵 B+ 树。
根据叶子节点的内容,索引类型分为主键索引和非主键索引。
主键索引的叶子节点存的是整行数据。在 InnoDB 里,主键索引也被称为聚簇索引(clustered index)。
非主键索引的叶子节点内容是主键的值。在 InnoDB 里,非主键索引也被称为二级索引(secondary index)。
主键索引和普通索引的查询区别:非主键索引的查询需要多扫描一棵索引树。
如果语句是 select * from T where ID=500,即主键查询方式,则只需要搜索 ID 这棵 B+ 树;
如果语句是 select * from T where k=5,即普通索引查询方式,则需要先搜索 k 索引树,得到 ID 的值为 500,再到 ID 索引树搜索一次。这个过程称为回表。
mysql> create table T( id int primary key, k int not null, name varchar(16), index (k))engine=InnoDB;
表中 R1~R5 的 (ID,k) 值分别为 (100,1)、(200,2)、(300,3)、(500,5) 和 (600,6),两棵树的示例示意图如下。
1)索引维护
B+ 树为了维护索引有序性,在插入新值的时候需要做必要的维护。以上面这个图为例,如果插入新的行 ID 值为 700,则只需要在 R5 的记录后面插入一个新记录。如果新插入的 ID 值为 400,就相对麻烦了,需要逻辑上
挪动后面的数据,空出位置。
而更糟的情况是,如果 R5 所在的数据页已经满了,根据 B+ 树的算法,这时候需要申请一个新的数据页,然后挪动部分数据过去。这个过程称为页分裂。在这种情况下,性能自然会受影响。
除了性能外,页分裂操作还影响数据页的利用率。原本放在一个页的数据,现在分到两个页中,整体空间利用率降低大约 50%。
当然有分裂就有合并。当相邻两个页由于删除了数据,利用率很低之后,会将数据页做合并。合并的过程,可以认为是分裂过程的逆过程。
自增主键的插入数据模式,正符合了我们前面提到的递增插入的场景。每次插入一条新记录,都是追加操作,都不涉及到挪动其他记录,也不会触发叶子节点的分裂。
主键长度越小,普通索引的叶子节点就越小,普通索引占用的空间也就越小。
覆盖索引
如果执行的语句是 select ID from T where k between 3 and 5,这时只需要查 ID 的值,而 ID 的值已经在 k 索引树上了,因此可以直接提供查询结果,不需要回表。也就是说,在这个查询里面,索引 k 已经“覆盖了”我们的
查询需求,称为覆盖索引。
由于覆盖索引可以减少树的搜索次数,显著提升查询性能,所以使用覆盖索引是一个常用的性能优化手段。
2)最左前缀原则
CREATE TABLE `tuser` ( `id` int(11) NOT NULL, `id_card` varchar(32) DEFAULT NULL, `name` varchar(32) DEFAULT NULL, `age` int(11) DEFAULT NULL, `ismale` tinyint(1) DEFAULT NULL, PRIMARY KEY (`id`), KEY `id_card` (`id_card`), KEY `name_age` (`name`,`age`) ) ENGINE=InnoDB
(name,age)联合索引
索引项是按照索引定义里面出现的字段顺序排序的。
当需求是查到所有名字是“张三”的人时,可以快速定位到 ID4,然后向后遍历得到所有需要的结果。
如果要查的是所有名字第一个字是“张”的人,SQL 语句的条件是"where name like ‘张 %’"。这时,也能够用上这个索引,查找到第一个符合条件的记录是 ID3,然后向后遍历,直到不满足条件为止。可以看到,不只是
索引的全部定义,只要满足最左前缀,就可以利用索引来加速检索。这个最左前缀可以是联合索引的最左 N 个字段,也可以是字符串索引的最左 M 个字符。
如何安排索引内的字段顺序:
-
- 如果通过调整顺序,可以少维护一个索引,那么这个顺序往往就是需要优先考虑采用的。
- 考虑的原则就是空间。比如上表的情况,name 字段是比 age 字段大的 ,建议创建一个(name,age) 的联合索引和一个 (age) 的单字段索引。
索引是key index (a,b,c),可以支持 (a) ,(a,b),(a,b,c),(a ,c ) ,但不支持 (b),(c),(b,c)进行查找。顺序可以不会影响,优化器会优化。测试:
CREATE TABLE `left_test` ( `id` int(11) NOT NULL, `a` int(11) DEFAULT NULL, `b` int(11) DEFAULT NULL, `c` int(11) DEFAULT NULL, `d` int(11) DEFAULT NULL, `e` int(11) DEFAULT NULL, PRIMARY KEY (`id`), KEY `index_abc` (`a`,`b`,`c`) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
索引 where > , < , >= , <= 都支持索引,!= 索引失效(当然最后具体索引有没有失效,要看优化器选择。比如如果这个表id 最大是3,那么查id < 4 就不会走索引,因为走索引和不走索引都是全表了,优化器会选择放弃索引)
3)索引下推
以市民表的联合索引(name, age)为例。
mysql> select * from tuser where name like '张%' and age=10 and ismale=1;
1. 能用 “张”,找到第一个满足条件的记录 ID3。
2. 在 MySQL 5.6 之前,只能从 ID3 开始一个个回表。到主键索引上找出数据行,再对比字段值。
而 MySQL 5.6 引入的索引下推优化(index condition pushdown), 可以在索引遍历过程中,对索引中包含的字段先做判断,直接过滤掉不满足条件的记录,减少回表次数。
InnoDB 在 (name,age) 索引内部就判断了 age 是否等于 10,对于不等于 10 的记录,直接判断并跳过。只需要对 ID4、ID5 这两条记录回表取数据判断,就只需要回表 2 次。
4)B+tree
1. B+节点关键字搜索采用闭合区间
2. B+非叶节点不保存数据相关信息,只保存关键字和子节点的引用
3. B+关键字对应的数据保存在叶子节点中
4. B+叶子节点是顺序排列的,并且相邻节点具有顺序引用的关系