专栏内容:
- postgresql内核源码分析
- 手写数据库toadb
- 并发编程
开源贡献:
- toadb开源库
个人主页:我的主页
管理社区:开源数据库
座右铭:天行健,君子以自强不息;地势坤,君子以厚德载物.
B树索引在PostgreSQL中得到了广泛应用,它是一种自平衡树数据结构,可以维护有序数据并允许进行搜索、顺序访问、插入和删除操作。在PostgreSQL中,可以在任何数据类型上使用B树索引,支持排序,支持大于、小于、等于、大于或等于、小于或等于的搜索。
B树具有一些重要的特征。首先,B树是平衡的,每个叶子页与根都由相同数量的内部页分隔开,因此搜索任何值都需要花费相同的时间。其次,B树是多分支的,每个页面通常包含许多(数百个)ctid,因此B树的深度很小,对于非常大的表,实际上可以达到4-5的深度。最后,索引中的数据按非递减顺序存储(在页面之间和每个页面内部),并且同一级别的页面通过双向列表相互连接,因此不需要每次都返回到根,可以通过遍历链表获取一个有序的数据集。
PostgreSQL中的B树索引是一种高效的数据结构,可以用于加速对有序数据的搜索和访问。通过使用B树索引,可以大大提高数据库的性能和响应时间。
本文主要介绍postgresql 中常用的索引类型btree的插入过程,通过本文对postgresql 索引查询的代码有一定了解,希望能够帮助基于postgresql做内核开发的同学,当然理解也有限,正在看这部分的同学可以在评论区一起来探讨。
在进行SQL语句 insert一条数据行时,如果某一列带有索引,那么在插入数据的同时,也需要插入一条索引项;
索引项中需要记录数据行的tid,也就是位置信息,所以插入索引的时机,是在插入数据行成功后,使用数据行的位置信息生成一条索引项,然后进行索引项的插入流程;
索引项的插入整体流程如下:
生成一个索引项,这就是需要插入的新索引项;
先是遍历btree树,从root中的元组进行二分查找,找到该索引项对应的范围所在的下层的节点的pageno,如果层级较多,每次都是如此,最终找到叶子节点,找到要插入的叶子节点的索引数据块;
对于主键的列,需要检查索引的唯一性,避免插入两条相同的索引;
对于主键肯定是需要唯一的存在,为什么要在这里做唯一性检查,而不是在数据插入时检查呢?
因为在插入数据时,如果检查唯一性,还是要通过索引扫描来比对,所以直接放到索引插入阶段,当索引检查不通过时,那么前面插入的数据也是无效的。
在前一步已经找到了符合的叶子节点,在 _bt_findinsertloc 中,还需要进一步查找合适的位置,主要有以下几种情况:
在上面向右查找的情况,postgresql做了一些优化,综合了向右找的代价,和提前分裂的代价,有概率提前分裂;
对于新插入的索引项,不能大于空闲空间的三分之一;
在 _bt_insertonpg 调用中,实现了剩余空间检查,页面分裂,索引项的真正插入;
当前查找到索引页空间小于当前新索引项时,就需要进行索引页的分裂;
如果空间足够,则直接插入即可;
Btree的页面分裂是插入环节中的关键点,也是难点所在;在这个环节中,因为会修改多个节点页,对并发访问影响比较大,postgresql在这里做了一些优化;
我们先按常规思路来模拟一下分裂的过程;
某一节点分裂成左右两个节点,将旧节点的内容平分到新节点中,然后变更左节点的后续链接,变更右节点的前续和后续链接,将它中加入本层的双向链表,最后将新增节点信息加到父节点中;
这样一个分裂的过程中,所有变更节点都需要加互斥锁。在btree查找章节中已经介绍过,上下层之间是单向的,也就是只能从父节点找到下层节点,为了避免重复查找父节点,在确定插入节点时,就已经将路的层次路径记了下来;
下面我们来看postgresql中如何进行分裂,以及进行了那些地方的优化;
按分裂的节点类型,分为根节点,页子节点,中间branch节点的分裂;
在一开始,数据不多时,根页面节点就足够存下了,此时根节点也是叶子节点;随着数据的增多,根节点就需要分裂了。
它的分裂不同于其它类型,下面图示分析:
当root节点分裂时:
1.节点分裂
btpo_flags
设置为分裂中,并且插入highkey,也就是右节点的最小值;2.更新父节点
在通过pginspect插件看时,就会发现root节点的pageno随着数据增加会一直变动,其实就是树的层次在增加,每增加一层级时,就会新创建一个root页面;
页子节点页面的分裂,是最常见的一种,当然也经过了很多的优化,比如对于相同键值的数据;还有对于多版本跨节点存储时,会再插入一条索引项,还有对于NULL值的存储;
先来看一下叶子页面分裂的整体流程示意图:
叶子节点分裂如下:
1.节点分裂
btpo_flags
设置为分裂中,并且插入highkey,也就是右节点的最小值;2.更新父节点
branch节点,是一种btree中间层的节点类型,它们存储的都是下层索引节点的页面位置和对应页面上的最小值;只有叶子节点上才会存储数据页的信息;
branch节点的分裂,类似于叶子节点的分裂;
只是在更新父节点时,多了一步,将left节点的正在分裂标志取消,也就是对于中间层的分裂已经完成;
如果不发生分裂,那么插入的流程如下:
因为btree索引是一种有序的索引,也是存储有序的,所以增加一条索引项时,如果在插入位置没有空槽位,就需要将当前位置及后面的索引项,向后移一个槽位,再将新索引项插入;
在整个插入过程中,查找过程,还有分裂时持有多个块的情况进行了优化;
主要通过以下措施:
这两个过程中,只会对当前页面加锁;
在索引扫描时,结束一个索引页面就会释放,再加下一个索引页面加锁,这样保持了很好的并发访问性能;
索引项只记录向下的关系,所以在扫描过程中,会记录父子关系的stack,这方便在分裂时,向上递归;
在分裂的时候,会加多个节点的锁,加锁原则是从左到右,避免死锁发生;
分裂时减少了加锁的节点数量,从叶子节点开始,叶子页面分裂时,会加左节点和右节点的锁;当更新父节点时,加锁父节点后,就释放了右节点的锁;整个过程中持有的锁,可能最多就是三个节点,当然还有短暂持有的锁,如meta节点,还有待分裂节点的右节点的锁,最多时会有四个节点;
记录叶子层的rightmost,对于null直接插入还有批量顺序插入时,直接就可以从fastpath找到插入节点,也就是叶子节点的最右节点;如果不符合时,再遍历查找;
这里使用了条件锁,得不到时,也会进行遍历查找,增加并发访问性能;
当树的某一层只有一个节点时,那这个节点就是fastroot,不用从root进行遍历,而从fastroot遍历就可以,加快速度;
因为索引页面不像数据页面是多版本机制,为了保持索引的存储精炼,而采用了原地更新,这就需要在更新时,如果服务异外宕机了,数据还能保持一致性和完整性;
如果没有发生页面分裂,在插入数据时发生了异常,此时是由WAL来恢复;WAL记录了插入的位置,以及原有数据位置的变化;
首先在分裂时,开始只将右节点加入了索引文件,分裂节点的数据并没有发生变化;此时异常并不会影响;
接下来,分裂节点数据发生变化,此时它的flag还是正在分裂中,那么数据其实是完整的,分别在左右两个页面上,只是从父节上只能找到左节点,从左节点再顺序通过后继链表就可以找到右节点,这在前面btree索引查找时就分享过。
非常感谢大家的支持,在浏览的同时别忘了留下您宝贵的评论,如果觉得值得鼓励,请点赞,收藏,我会更加努力!
作者邮箱:[email protected]
如有错误或者疏漏欢迎指出,互相学习。
注:未经同意,不得转载!