postgresql中的btree索引

基础知识
  • pg中b+树索引的节点,保留了向左,向右,向下的索引

  • 每个节点是一个页

  • 每个节点的第一个key是highkey( 表示此节点或者此节点的子节点的最大值 ),除了每层最右节点(没有highkey,第一个key用firstkey表示)

  • 索引的第0页是元数据页

  • pg的btree 不限制一页的索引数量,但限制每条索引的长度,这是为了支持可变长度变量而做的考虑。

  • 为了平衡每页的占用量,pg在计算页占用时会把要插入的索引的长度也算进去,以避免可能找到了合适的插入页的位置,但这页的空间不够这个索引插入的尴尬

  • pg没有实现在节点从超过半满到降回半满时的merge操作,因为太复杂了

对于相同key的处理
  • 当要插入一个索引,发现key与这页(叶子节点)的 highkey 一样,这时要看下一页(右叶子节点)与这一页哪页的空闲位置多,就插在哪一页
btree上的锁
  • pg的加的锁是页级的读锁,这会影响其它进程写的效率,因而当扫描到要的索引后,会先把这些索引对应的id拷贝一份,然后释放锁,不必一直持有锁,增加效率、
  • pg为了支持后向扫描而加入了左 sibling 的指针,因而在节点分裂时,相比不需要维护左指针的b树而言,需要额外锁 右sibling 的节点,以防返向读的那个进程能访问到这个正在分裂的页。
  • 在顺序扫描叶子节点时要对左叶子节点加pin,以防止左叶子节点被删除,而使后向扫描失败。
  • 同样由于支持后向扫描,在做后向扫描时要先找到左sibling,加锁,这样可以防止左节点分裂。
  • 分裂的存在使得,扫描右页时检查一下当前页是不是分裂了一半的状态,如果是说明父节点的downlink还没有指向这个右页就挂了,这时要去重新加上父节点向右页的downlink。
  • 当向左扫描一页时,是扫描到边界时才去锁左页,过早锁页可能因左页分裂而遗漏一些元组
BTree节点的分裂
  • 一般节点:分裂时,先标记自己为half-dead,创建新左右节点,把原节点的一半复制到左节点,一半到右节点上,再链接进原有的链中。第二步要建立父节点到右节点的downlink,一切结束后才把这个half-dead标记清除(相当于是一个原子操作的标记,如果这两步中间挂了,下一次恢复后,扫描一页发现是half-dead,则要去补充父节点到右节点的downlink,为什么在扫描时而不在recovery的vacuum扫描阶段呢:最后中途崩溃的恢复有说明)

  • 在顺序扫描叶子节点时,pg 会记住右节点的页号,如果当前页扫描完发现下一页的页号变了,说明当前页可能发生了分裂,要跳过下一页的一些元组以防止重复扫描。

  • 根的处理有一点点不同:先分裂出右页,再创建根,最后将元数据的pointer指向新根(元数据要最后更新)

BTree节点的删除
  • vacuum在回归元组空间时会先删除索引,后删除元组,可能存在正有进程访问索引对应的元组,回来发现自己的索引没了,为了避免这种冲突,pg引入super-exclusive 锁,要求上锁的页不能有 pin。

  • 由于最右节点不会删除,树的高度不会减少:标记最低的那个根为快根,每次分裂和删除时要检查和更新快根

  • 删除叶子页有两步:

    • 从parent上移除指向自己的downlink,标记自己为half-dead(扫描遇到half-dead页时会跳过它)

      • 锁定父节点和自己

      • 使自己的downlink指向右sibling,相当于变成了一个中间页,没有downlink指向自己,而自己的downlink指向右兄弟节点(这里相当于一个trick,当它与右节点不能合并时可以保证顺序扫描依然能进行)

      • 如果它与右sibling节点有同一个parent,就将它与右节点合并,更新highkey和父节点的downlink。如果不是,考虑到非叶子节点的合并不是原子操作,因而不去做合并。(这样的结果是,如果父节点有多于一个的子节点,那么这个最右节点不会合并范围,直到只剩它自己时再,删除父节点,过程见下面。)

    • 与右节点合并

      • 上锁:左sibling,自己,右sibling
      • 更新 sidelink
      • 标记自己为删除(之后vacuum时会被放在FSM中)
  • 当孩子节点只剩一个时,删除最后一个孩子节点:
    • 断开父与爷之间downlink
    • 如果父也是爷的惟一的孩子,则向上找有多于一个孩子的父节点,移除downlink
    • 把自己标记为half-dead
    • 从上向下,将每一层中节点与左右sibling之间的 link 删除
向左(逆序)扫描算法
  • 先向左,如果左节点是活的,那就是它
    • 否则这个左节点可能分裂了,从这个左节点开始向右找自己的邻接左节点
  • 找到左节点后判断自己是不是还活着,如果dead了,就向右移动至一个活节点,重复第一步
空间的回收 vacuum
  • 空间回收使用的是最强最简单的条件:没有快照引用这个页了
  • 回收的页会放在FSM中
  • 回收是按照页号进行顺序扫描的,当发现有一个页在回收开始后分裂了,且分裂的右节点比当前页号小,为了覆盖的完全,需要先去回收那个分裂的小页号的页,再接着回收当前页
    • 具体实现:在扫一页前,要判断是否这个叶在本次vacuum进行时分裂了(即页上标记的分裂时 cycle id 是本次vacuum的cycle id,如果分裂则要看看它的右页页号是不是小页号,如果是,则这个右页没被vacuum扫描过,就要先扫描右页,然后才回到这个页号,按页号继续向前。
一致性的保证(WAL日志)
  • 写 write ahead log 日志的时间点有:
    • 插入索引
    • 分裂一个节点
    • 生成新的根
    • 叶节点标记half-dead并移除downlink
    • 移除左右 sibling 向自己的 link
中途崩溃的恢复
  • 在插入时发现某个节点标记正在分裂,说明还没有建立 父节点的 downlink,重新建立

  • 之所以选择在插入时而不在vacuum扫描时重新建立downlink,是因为建立downlink时有可能导致分裂页,需要占用磁盘,而之前崩溃有可能就是因为磁盘满了才导致的,将会使vacuum失败,磁盘永远满下去。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值