数据库的索引是一个要点, 无论是面试还是在工作中, 这个知识点都很常会用到, 你可能只是用过索引, 知道加了索引可以提高查询的性能, 但不知道为什么这样, 今天我们一起来详细了解下吧.
索引有很多种存储结构, 称之为索引模型, 不同类型的模型分别对应不同的适用场景.
哈希表是一种以键值对存储数据的结构 KEY - VALUE. 查找时输入 key 来查找对应点 value.
哈希表的思路很简单, 将值放置到数组中. 如定义一个长度为 16 的数组, 输入 key: user1, 对 user1 做哈希运算 (利用哈希函数), 返回一个整数, 如 2156648, 用这个数对 16 取余, 返回值为 8. 那么就将他放到数组的第 8 个位置.
你可能会有下面的疑惑:
HashMap 的存储方式.总结: 适合用于等值查询, 不适用于范围查询. 出现大量哈希冲突的情况后, 查询效率会很低.
这个就更简单了, 将所有值从小到大排序, 这样查找时, 可以采用二分法, 时间复杂度只有 O(logN). 而且对范围查询的支持也非常好, 先根据二分法, 找到范围查询的左值, 然后依次遍历数组到范围查询的右值即可.
但这仅仅是查询效率很好, 但向数组中心插入值就麻烦了, 如现有数据 [1, 5, 8, 10, 11, 13], 现在要插入数据值 3, 那么就要将 5, 8, 10, 11, 13 这些值都向后移动一位, 插入操作的效率为 O(N), 这并不是一个理想的效率.
适用于范围查询. 等值查询的效率也较高, 但插入操作效率较低.
二叉搜索树的特点是: 每个节点的左儿子小于父节点, 父节点又小于右儿子. 如:
5
/ \
3 6
/ \ \
2 4 8
/ / \
1 7 9这是如果你找值 7, 先从根节点 5 开始找, 比 5 大, 那肯定在右边, 所以找到第二层的 6, 比 6 还大, 找到第三次的 8, 比 8 小, 最后找到第四层的 7, 这样最坏的情况也就数据在树的最后一层, 即平均时间复杂度为 O(logN).
但是二叉搜索树还可能会出现一个问题是树不平衡, 如:
1
\
2
\
3
\
4
\
5
\
6
\
7
\
8
\
9上面我们提高二叉搜索树的查询性能取决于树的高度, 现在这个数查询的性能变成了 O(N), 性能大打折扣. 为了解决这个问题, 提出了 红黑树 的概念. 他是一种自平衡的二叉搜索树, 即在插入节点时, 判断整个树是否是平衡的, 如果不平衡, 通过一系列旋转操作来达到平衡的目的, 在更新时维持树的平衡需要的时间复杂度也是 O(logN).
红黑树的篇幅有点长, 不太适合放到这里, 需要详细了解的朋友, 可自行查阅相关知识.
同样的, 平衡二叉树也有一个问题, 如当数据有 100 条时, 此时树的高度为 20, 那么一次查询可能需要访问 20 个数据块, 也就是访问 20 次磁盘, 这是不能被接受的, 尤其是在机械硬盘下, 为了让一个查询更少的读取磁盘, 我们就不应该使用二叉树, 而是 N 叉树, 这个 N 取决于数据块的大小.
以 InnoDB 的一个整数字段索引为例, 这个 N 差不多是 1200, 当树高为 4 时, 可以存储 1200 的三次方个值, 大约为 17 亿, 那么这样访问磁盘的次数就大大减少了.
InnoDB 中的 B+ 树, 就是 N 叉树的一种实现.
在 InnoDB 中, 表是根据主键顺序以索引的形式存放的, InnoDB 存储模型采用了 B+ 树索引模型, 在 InnoDB 中每一个索引都对应着一颗 B+ 树, 每棵非主键索引树的叶子节点存储的是主键的值, 每棵主键树的叶子节点存储的是整行数据值.
假如, 我们有一个主键列为 ID 的表, 表中有字段 K, 并且在 K 上有索引, 建表语句如下:
create table T
(
id int primary key,
k int not null,
name varchar(16),
index (k)
) engine = InnoDB;然后插入数据:
insert into T(id, k, name) values(100, 1, "A");
insert into T(id, k, name) values(200, 2, "B");
insert into T(id, k, name) values(300, 3, "C");
insert into T(id, k, name) values(400, 4, "D");
insert into T(id, k, name) values(500, 5, "E");
insert into T(id, k, name) values(600, 6, "F");两个树的示意图如下:

那么对于 主键索引和普通索引的查询有什么区别呢?
如查询语句 select * from T where ID = 500, 即根据主键进行查询, 则只需要搜索 ID 索引树.
如查询语句 select * from T where K = 5, 即根据普通索引进行查询, 则需要先搜索 K 索引树, 得到 ID 值 500, 再到 ID 索引树搜索. 这个过程称之为回表.
由于普通索引的叶子节点存储的是主键, 那么很显然, 主键长度越小越好, 所以自增主键是一个很好的选择.
当然, 如果你自己有业务字段是唯一的, 且不需要其他索引, 那么使用业务字段来做主键会适合.
还是上面那个查询语句: select * from T where K = 5, 上面我们他会进行 回表 操作.
那么有没有可能经过索引优化, 不回表呢?
如果执行的语句是: select ID from T where K = 5, 这时只需要查找 ID 值, 而 ID 值已经在 k 索引树的叶子节点中了, 这样已经得到了需要的数据, 就不用进行 回表 操作了.
在这个查询中, 索引 k 覆盖了我们需要查询的字段, 我们称之为 覆盖索引.
由于覆盖索引可以减少树的搜索次数,显著提升查询性能,所以使用覆盖索引是一个常用的性能优化手段。
当然, 我们不能为所有需要查询的字段都建立上 索引, 那索引就太多了, 并且索引的维护成本也很大, 其实 B+ 树 这种索引结构, 支持最左前缀匹配, 来定位记录.
如现在我们有 (name, age) 这个联合索引 :

可以看到, 索引已经按照定义中的顺序排序好了, name 在前, age 在后, 如果 name 一致, 按照 age 排序.
此时我们需要查找所有姓名等于 “张三” 的人, 可以快速定位到 ID4, 然后向后遍历直到姓名不等于张三.
那么如果你要查找的是所有姓名的第一个字等于 “张” 的人, 也可以快速定位到 ID3, 然后向后遍历直到不符合条件.
同理, 如现在有联合索引 (name, age, score), 那么我们查询 where name = '%张' and age = 10 也是用到了索引的, 但查询 where name = '%张' and score = 60, 就没有完全用到这个索引.
由此可知, 我们只要满足索引的最左前缀, 就可以用索引来加速检索, 这个最左前缀可以是联合索引的最左 N 个字段, 也可以是字符串索引的前 M 个字符.
上一段我们提到了最左前缀可以用来在索引中定位记录, 但如果不符合最左前缀的部分, 应该怎么办呢?
还是以 (name, age) 联合索引为例. 现在需要查询 “名字第一个字是张, 年龄为 10 的男孩”, sql 示意如下:
select * from tuser where name like '张%' and age = 10 and ismale = 1;再次附上索引结构图:

这个索引只能用到 name 的前缀索引, 找到第一个满足条件的记录 ID3, 然后, 如何判断后面两个条件是否满足呢?
在 MySQL 5.6 之前, 只能从 ID3 开始一个一个的回表, 到主键索引上找出数据行, 再比对字段值.
而在 MySQL 5.6 引入了索引下推优化, 即在索引遍历过程中, 对索引中包含的字段先做判断, 先过滤到不符合条件的记录, 避免回表:
无索引下推执行流程:

有索引下推执行流程:

每个虚线表示回表一次, 在无索引下推图中, 我特意去掉了 age 值, 因为他不会看 age 的值, 只是按顺序把 name 第一个字是 “张” 的所有数据一个一个回表, 因此回表了 4 次.
而在有索引下推时, InnoDB 在 (name, age) 索引内部就判断了 age 是否等于 10, 对于不等于的, 直接跳过, 所以这里只回表了 2 次.