1. 前言
我们都知道: 在编写SQL语句的时候, 合理地使用索引, 能够大大地加快查询的速度(关于如何使用索引这个问题, 后面会再单独写一篇文章来介绍 — 理解清楚SQL语句中, 什么时候会走索引, 什么时候索引会失效…), 但是会拖慢增删该的速度, 会比较占用额外的空间内存, 所以也就说了, 索引是使用在少些多读的场景下的.
那么索引的底层又是使用的是哪种数据结构呢? 是顺序表? 二叉搜索树? 哈希表? 红黑树? 还是其他呢? 请继续往下看.
2. 谈谈索引底层用的数据结构
前情提要: 一般情况下, 我们在设置外键的时候, 经常就会使用到索引, 因为在一个表中的某一个字段设置外键的时候, 会连接另外一个表的某一个字段, 这一点在MySQL基础语法里面就有讲到过. 那么在使用外键时候, 通过索引来进行查询, 速度就会加快不少.
但是这里有一点需要注意的是: 不是说创建了外键就会自动触发索引, 而是我们一般使用外键连接的字段是另外一个表的主键. 这时候就会引出了索引的创建相关的问题, 简单说一下: 索引的创建包含两种, 一是自动创建; 二是手动创建.
自动创建是指 — 数据库在我们对某一个字段进行约束的时候, 就自动帮我们创建好一个索引, 索引名就是约束名, 在数据库中有两种约束会自动创建索引(主键约束: primary key; 唯一约束: unique). 而手动创建的话需要根据自己建表的需求来创建索引, 具体可以是什么情况就不做过多描述, 我们还是继续来讲讲索引底层的数据结构, 关于手动创建索引的内容有兴趣可以自己了解(或者等恶后面的文章…).
2.1 是顺序表 / 链表吗?
先来说说 链表 , 链表其实是第一个被排除的, 链表对单纯的新增节点/删除节点是会比较高效的, 但是对于查找, 需要从头开始遍历链表, 时间复杂度达到了 O(n) , 很显然是不合适的 . (这里需要插一嘴: 在Java中, 我们使用链表是用到LinkedList这个类, 而这个类中实现指定位置插入节点的方法是需要先遍历链表到指定位置再进行插入的, 所以整个插入节点的操作时间复杂度是 O(n) , 在这一点上很多人误以为是 O(1).).
再来说说 顺序表 , 其实顺序表也是和链表一样, 如果在进行索引的时候, 还是需要从头开始遍历的. 这时候就会有人问了: "不是说顺序表在进行查找的时候效率比链表高吗? 为什么说是一样的? ", 这里所谓的查找效率高, 其实是ArrayList支持下标随机访问的, 我们可以根据下标快速地查找到这个下标对应的元素, 但是我们这里是在数据库中进行索引的操作, 是不可能通过下标来快速定位的, 只能是从头开始进行遍历, 这时候的时间复杂度还是 O(n) , 这个时间复杂度, 对于索引来说肯定是不能接受的, 所以, 很显然是不合适的 .
2.2 是二叉搜索树 / AVL树 / 红黑树吗?
接下来就来谈谈 二叉搜索树 吧~ 一开始, 很多人听到这个名字 — “二叉搜索树”, 又是二叉, 有能搜索, 这肯定行吧. 确实, 很多情况下, 二叉搜索树在进行搜索的时候, 时间复杂度能达到 O(logn) , 但是是会有特殊情况的, 当这棵二叉搜索树是一种高度不平衡的时候(也就是整棵树都只有右孩子或者只有左孩子的情况), 那这棵树就无异于是一个变相的链表, 最坏的情况下, 时间复杂度也还是达到了 O(n) .所以, 很显然也是不合适的 .
这时候, 就会再想到: 既然二叉搜索树有特殊情况, 那么我们不是只需要将这些特殊情况排除掉, 就万事大吉了嘛. 很多人就开始会想到了一些改良之后的二叉搜索树, 就比如 红黑树 / AVL树 . 先简单地说说红黑树和AVL树(由于还没有详细学习到这部分知识, 所以这里只能做一个简单的介绍, 后面会再出这部分更加详细的内容), 红黑树和AVL树相对于普通的二叉搜索树引入了一些规则, 这些规则可以是这棵二叉搜索树变得高度平衡起来, 不至于可能变成一个变相的链表. 所以, 这时候它们在进行查找操作的时间复杂度就基本可以判定为是 O(logn). 相对于普通的二叉搜索树, 这样的时间复杂度已经是降低不少了, 但是这还不是索引底层所使用的数据结构, 因为红黑树/AVL数归根结底还是一颗二叉树, 在面对数据库中海量的数据的时候, 树的高度一下子就上来了, 树的高度一旦增加, 查询比较的次数也将会增加不少, 这样的话带来的影响将会是: 由于我们的数据较大且需要持久性的, 所以一般都是存储在磁盘里面, 当如果进行索引查找比较的话, 就会大量读取磁盘上的内容, 然后加载到内存当中进行比较, 这样的话, 比较的次数是非常巨大的, 如若真的是这样的话, 那么我们大多数的电脑大概率就会直接卡死… 所以, 很显然也是不合适的 .
既然二叉会使数的高度飞速增加, 那么有人可能就又想到了: 不用二叉搜索树, 而是使用 N叉搜索树 , 然后在引进一些额外的规则, 让这棵N叉搜索树再变得高度平衡行不行? 答案是不行的. 其实在面对海量的数据的时候, N叉与二叉基本是没有什么区别的, N叉只是可以分出更多的节点出来而已, 可解近忧但是解不了远虑, 所以 很显然也是不合适的 .
2.3 是哈希表吗?
除了上面那些数据结构, 我们剩下最基本的数据结构不就剩下 哈希表 了嘛. 这时候, 有人可能会说了: 哈希表查询的效率是最高的, 时间复杂度达到了 O(1). 这里要解释: 哈希表确实是查询效率最高的, 这一点上是毋庸置疑的, 但是哈希表会存在一个弊端 — 不能够进行数据的指定范围查找或者是模糊查找, 哈希表中的键和值是一一对应的关系, 也就是说哈希表只适用于快速地查找相等的情况, 在这一点上 索引的底层实现也就不能考虑使用哈希表了 .
2.4 真正底层的数据结构原来是这!
最基本的数据结构也基本在上面被排查完了, 有人就提出了其它的比较经典的数据结构 — B树 . 简单粗暴地手画一下B树.
类似这样的, 与普通的二叉搜索树不同的, 一个节点上可以存储多个数据, 每两个数据中间又可以再细分出两数据的中间值进行数值比较的数. 这种就可以称为B数. 相比于二叉搜索树的高度(以2为底的对数), B树这样的以N为底的对数的高度, 大大地降低了树的高度. 但是B树还不是索引底层的数据结构. 索引的底层逻辑而是使用了B树的一种改良后的版本 — B+树 . 这里又要简单粗暴地手画一下B+树.
类似这样的, (1) 每个节点上可以存储多个值, 每个节点可以分出多个子树; (2) 父节点上的元素都在子节点在存在, 且是子节点上的最大值或者最小值; (3) 最后的叶子结点使用的是链表来将它们串联起来. 就称为是B树的进化版本 — B+树.
可以这样说, 索引所需求(要求)的, B+树身上都有存在(符合)的, 具体原因可以分为下面这两个点:
(1) 索引要求查询速度要快 . 正好, B+树是类似与改良的二叉搜索树系列, 查询速度是非常快的. 其次, B+树还在叶子结点串成一个链表, 这样的话, 如果再进行下一次查找可能就不再需要通过树查询了, 而可以通过链表上一次节点来判断下一次索引相对的前后位置, 从而进一步提高查询效率. 再者, B+树因为可以单个节点可以存放多个数据, 从而使树的高度进一步缩减, 查询速度又一步提高.
(2) 索引要求尽量不要对磁盘进行大量读写操作 . 正好, 每一个数据行, 只需要保存下B+树中的叶子结点即可, 非叶子结点不需要存储到真实的数据行里面, 只需要用来做存储ID就足够了. 由于使用ID进行存储, 使得非叶子结点占用的空间是非常小的(比起整行数据直接存储要小得多的多), 就可能可以把非叶子结点加载到内存中进行快速地查找, 就不在需要像其他数据结构那样, 将数据存储到磁盘里面, 减少了大量的读写操作, 大大降低磁盘IO.
3. 总结
本文最终目的已达到: 浅识B+树 -> B+树的优势 -> 以及索引的底层数据结构是B+树 -> 使用B+树的原因…
索引的底层数据结构只是数据库索引中的一小部分, 要想真正学透数据库, 还是任重而道远啊~
接下来, 我有打算开始下一篇文章: SQL的性能调优问题. 进一步理解SQL语句什么时候会走索引? 什么时候索引会失效? 这是否与SQL语句的写法有关?