1、二叉树
二叉搜索树是最为大家所熟知的一种数据结构,就不展开介绍了,它为什么不适合用作数据库索引?
(1)当数据量大的时候,树的高度会比较高,数据量大的时候,查询会比较慢;
(2)每个节点只存储一个记录,可能导致一次查询有很多次磁盘IO;
2、B-Tree
IO次数就是树的高度,而“矮胖”就是b树的特征之一, B树属于多叉树又名平衡多路查找树(查找路径不只两个)
B树,如上图,它的特点是:
(1)不再是二叉搜索,而是m叉搜索;
(2)叶子节点,非叶子节点,都存储数据;
(3)中序遍历,可以获得所有节点;
画外音,实在不想介绍这个特性:非根节点包含的关键字个数j满足,(┌m/2┐)-1 <= j <= m-1,节点分裂时要满足这个条件。
B树被作为实现索引的数据结构被创造出来,是因为它能够完美的利用“局部性原理”。
什么是局部性原理?
局部性原理的逻辑是这样的:
(1)内存读写块,磁盘读写慢,而且慢很多;
(2)磁盘预读:磁盘读写并不是按需读取,而是按页预读,一次会读一页的数据,每次加载更多的数据,如果未来要读取的数据就在这一页中,可以避免未来的磁盘IO,提高效率;
画外音:通常,一页数据是4K。
(3)局部性原理:软件设计要尽量遵循“数据读取集中”与“使用到一个数据,大概率会使用其附近的数据”,这样磁盘预读能充分提高磁盘IO;
B树为何适合做索引?
(1)由于是m分叉的,高度能够大大降低;
(2)每个节点可以存储j个记录,如果将节点大小设置为页大小,例如4K,能够充分的利用预读的特性,极大减少磁盘IO;
总结:从平衡二叉树、B树、B+树、B*树总体来看它们的贯彻的思想是相同的,都是采用二分法和数据平衡策略来提升查找数据的速度。
3、B+Tree
B+树,如上图,仍是m叉搜索树,在B树的基础上,做了一些改进:
(1)非叶子节点不再存储数据,数据只存储在同一层的叶子节点上;
画外音:B+树中根到每一个节点的路径长度一样,而B树不是这样。
(2)叶子之间,增加了链表,获取所有节点,不再需要中序遍历;
这些改进让B+树比B树有更优的特性:
(1)范围查找,定位min与max之后,中间叶子节点,就是结果集,不用中序回溯;
画外音:范围查询在SQL中用得很多,这是B+树比B树最大的优势。
(2)叶子节点存储实际记录行,记录行相对比较紧密的存储,适合大数据量磁盘存储;非叶子节点存储记录的PK,用于查询加速,适合内存存储;
(3)非叶子节点,不存储实际记录,而只存储记录的KEY的话,那么在相同内存的情况下,B+树能够存储更多索引;
最后,量化说下,为什么m叉的B+树比二叉搜索树的高度大大大大降低?
大概计算一下:
(1)局部性原理,将一个节点的大小设为一页,一页4K,假设一个KEY有8字节,一个节点可以存储500个KEY,即j=500
(2)m叉树,大概m/2<= j <=m,即可以差不多是1000叉树
(3)那么:
一层树:1个节点,1*500个KEY,大小4K
二层树:1000个节点,1000*500=50W个KEY,大小1000*4K=4M
三层树:1000*1000个节点,1000*1000*500=5亿个KEY,大小1000*1000*4K=4G
画外音:额,帮忙看下有没有算错。
可以看到,存储大量的数据(5亿),并不需要太高树的深度(高度3),索引也不是太占内存(4G)。
总结
-
数据库索引用于加速查询
-
虽然哈希索引是O(1),树索引是O(log(n)),但SQL有很多“有序”需求,故数据库使用树型索引
-
InnoDB不支持哈希索引
-
数据预读的思路是:磁盘读写并不是按需读取,而是按页预读,一次会读一页的数据,每次加载更多的数据,以便未来减少磁盘IO
-
局部性原理:软件设计要尽量遵循“数据读取集中”与“使用到一个数据,大概率会使用其附近的数据”,这样磁盘预读能充分提高磁盘IO
-
数据库的索引最常用B+树:
(1)很适合磁盘存储,能够充分利用局部性原理,磁盘预读;
(2)很低的树高度,能够存储大量数据;
(3)索引本身占用的内存很小;
(4)能够很好的支持单点查询,范围查询,有序性查询;
4、MyISAM
索引文件
与数据文件
是分离
的MyISAM
的索引文件采用B+Tree
索引叶子节点data域
记录的是数据存放的地址
主索引(唯一)
与辅助索引(可重复)
在结构上没有任何区别
- 有连续聚集的区域单独存储行记录
- 主键索引的叶子节点,存储主键,与对应行记录的指针
- 普通索引的叶子结点,存储索引列,与对应行记录的指针
- 主键索引与普通索引是两棵独立的索引B+树,通过索引列查找时,先定位到B+树的叶子节点,再通过指针定位到行记录。
5、InnoDB
数据文件
本身是按照B+Tree
组织的索引结构(主索引:Primary Index
或聚集索引:Clustered Index
),而叶子节点data域
记录的是完整的数据信息,聚簇索引就是数据的存储方式(所有的用户记录都存储在了叶子节点),也就是所谓的索引即数据。
- InnoDB
必须有主键
,如果没有显式定义主键
或非NULL的唯一索引
,InnoDB会自动生成6 Bytes的ROWID
作为主键 辅助索引
(Secondary Index
)也是按B+Tree
组织,叶子节点data域
记录的是主键值
,因此主键不宜定义太大
- 搜索
辅助索引
需要遍历两遍索引
,首先通过辅助索引
获得主键值,再用主键值在主索引
中获取实际数据 -
InnoDB的主键索引与行记录是存储在一起的,故叫做聚集索引(Clustered Index):
-
没有单独区域存储行记录
-
主键索引的叶子节点,存储主键,与对应行记录(而不是指针)
6、示例
我们举个例子:
CREATE TABLE t (
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
key1 INT,
common_field VARCHAR(100),
PRIMARY KEY (id),
KEY idx_key1 (key1)
) Engine=InnoDB CHARSET=utf8;
这个表就包含2个索引(也就是2棵B+树):
-
以
id
列为主键对应的聚簇索引。 -
为
key1
列建立的二级索引idx_key1
。
我们向表中插入一些记录:
INSERT INTO t VALUES
(1, 30, 'b'),
(2, 80, 'b'),
(3, 23, 'b'),
(4, NULL, 'b'),
(5, 11, 'b'),
(6, 53, 'b'),
(7, 63, 'b'),
(8, NULL, 'b'),
(9, 99, 'b'),
(10, 12, 'b'),
(11, 66, 'b'),
(12, NULL, 'b'),
(13, 66, 'b'),
(14, 30, 'b'),
(15, 11, 'b'),
(16, 90, 'b');
所以现在s1
表的聚簇索引示意图就是这样:
s1
表的二级索引示意图就是这样:
从图中可以看出,值为NULL
的二级索引记录都被放到了B+树的最左边,这是因为设计InnoDB的大叔们有规定:
We define the SQL null to be the smallest possible value of a field.
也就是认为NULL
值是最小的。
小贴士:原谅我们把B+树的结构做了一个如此这般的简化,我们省略了页面的结构,省略了所有的内节点(只画了了三角形替代),省略了记录之间的链表,因为这些不是本文的重点,画成如果所示的样子只是为了突出叶子节点处的记录是按照给定索引的键值进行排序的。
比方说我们现在执行下边这个查询语句:
SELECT * FROM t WHERE key1 = 53;
那么语句的执行过程就如下图所示:
用文字描述一下这个过程也就是:
-
先通过二级索引
idx_key1
对应的B+
树快速定位到key1
列值为53
的那条二级索引记录。 -
然后通过二级索引记录上的主键值,也就是
6
到执行回表
操作,也就是到聚簇索引中再找到id
列值为6
的聚簇索引记录。
小贴士:B+树叶子节点中的记录都是按照键值按照从小到大的顺序排好序的,通过B+树索引定位到叶子节点中的一条记录是非常快速的。不过由于我们并没有唠叨内节点、页目录这些东西,所以通过B+树索引定位到叶子节点中的一条记录的过程就不详细唠叨了,这些东西其实都在《MySQL是怎样运行的:从根儿上理解MySQL》的掘金小册里详细讲述过。
像下边这个查询:
SELECT * FROM t WHERE key1 > 20 AND key1 < 50;
它的执行示意图就是这样:
用文字表述就是这样:
-
先通过二级索引
idx_key1
对应的B+
树快速定位到满足key1 > 20
的第一条记录,也就是我们图中所示的key1
值为23
的那条记录,然后根据该二级索引中的主键值3
执行回表操作,得到完整的用户记录后发送到客户端。 -
然后根据上一步骤中获取到的
key1
列值为23
的二级索引记录的next_record
属性,找到紧邻着的下一条二级索引记录,也就是key1
列值为30
的记录,然后执行回表操作,得到完整用户记录后发送到客户端。 -
然后再找上一步骤中获取到的
key1
列值为30
的二级索引记录的下一条记录,该记录的key1
列值也为30
,继续执行回表操作将完整的用户记录发送到客户端。 -
然后再找上一步骤中获取到的
key1
列值为30
的二级索引记录的下一条记录,该记录的key1
列值为53
,不满足key1 < 50
的条件,所以查询就此终止。
从上边的步骤中也可以看出来:需要扫描的二级索引记录越多,需要执行的回表操作也就越多。如果需要扫描的二级索引记录占全部记录的比例达到某个范围,那优化器就可能选择使用全表扫描的方式执行查询(一个极端的例子就是扫描全部的二级索引记录,那么将对所有的二级索引记录执行回表操作,显然还不如直接全表扫描)。
小贴士:我们这里还是定型的分析成本,而不定量分析。定量分析的过程比较复杂,不过小册里有写,有兴趣的同学可以去看。
所以现在的结论就是:判定某个查询是否可以使用索引的条件就是需要扫描的二级索引记录占全部记录的比例是否比较低,较低的话说明成本较低,那就可以使用二级索引来执行查询,否则要采用全表扫描。
Refer:
[1]InnoDB备忘录 - B+Tree索引
http://zhongmingmao.me/2017/05/13/innodb-btree-index/
[2] 平衡二叉树、B树、B+树、B*树 理解其中一种你就都明白了
https://zhuanlan.zhihu.com/p/27700617
[3] Does mysql use B-tree,B+tree or both?
https://dba.stackexchange.com/questions/204561/does-mysql-use-b-tree-btree-or-both
[4] MySQL的索引
[5] 数据库两个神器【索引和锁】
https://segmentfault.com/a/1190000015738121
[6] 设计 MySQL 数据表的时候一般都有一列为自增 ID,这样设计原因是什么,有什么好处?
https://www.zhihu.com/question/28703540/answer/494072901
[7] 机器学习能革了数据库索引的命吗?
https://mp.weixin.qq.com/s/o115JjjtUzJZ4MQ9yO-WuQ
[8] 收藏版MySQL语句加锁分析
[9] 快速理解为啥这个查询使用索引,那个查询不使用索引
https://mp.weixin.qq.com/s/cyr8rW9-iP_N-BWDyqIVEQ
[10] 面试官:为什么 MySQL 索引要使用 B+树而不是其它树形结构?比如 B 树?InnoDB一棵B+树可以存放多少行数据?
https://mp.weixin.qq.com/s/6j64s9W6ogs5Y8BbhhkgnA
来源:oschina
链接:https://my.oschina.net/u/568818/blog/75688