Post

PostgreSQL B树搜索

PostgreSQL B树搜索

PG使用B-Link Tree作为存储结构,本文分析其搜索流程

_bt_first第一次搜索B树,先处理key,处理并行扫描,决定比较模式(<, <=, =, >, >=),扫描顺序(向前或向后),初始化scankey,最终进入_bt_search搜索key所在页,然后调用_bt_binsrch二分搜索页中记录。

_bt_first中还会决定nextkey。若nextkey = false,_bt_search和_bt_binsrch定位到的item >= scankey;若nextkey = true,定位到的item > scankey。nextkey由比较模式和扫描顺序决定。

_bt_search使用key搜索B树,返回key可能所在的第一个叶子页

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
BTStack // 返回搜索栈
_bt_search(
    scankey // 搜索key
    nextkey // false则搜索第一个>= scankey的记录,true则搜索第一个> scankey的记录
    *bufP // 返回定位到的页
    access // BT_READ表示读B树,BT_WRITE表示会对B树进行修改,如插入、更新、删除等
)
{
    // 获取根页,上S锁
    _bt_getroot

    // 搜索到叶子层
    for (;;)
    {
        // 顺着downlink访问下层页时,由于无锁,下层页可能已经分裂,若分裂则key可能已经不再此页中了,需要右移。WRITE模式,此接口内可能会修复叶子页未完成的分裂,非叶子页的未完成分裂会在_bt_getstackbuf中修复,但在此接口内修复也无妨
        _bt_moveright // 非叶子页都是S锁

        // 此时抓到的页,一定是搜索key所在页

        if(这是叶子页)
            break;

        // 找到内页的pivot tuple,获取孩子页指针
        _bt_binsrch

        // 将搜索路径上的pivot tuple登记到搜索栈

        // 上面的搜索,对路径上的页上的锁都是S锁,如果到了叶子页上一层,且以BT_WRITE访问B树,则对叶子节点上X锁,否则还是S锁
        if (level == 1 && access == BT_WRITE)
            page_access = BT_WRITE;

        // 先释放父亲页的锁,再获取孩子页的锁
        _bt_relandgetbuf
    }

    // 若以BT_WRITE方式访问,但实际上以BT_READ方式访问了,即对叶子页上的是S锁
    // 说明B树只有一层,根页就是叶子页,此时需要加上X锁
    if (access == BT_WRITE && page_access == BT_READ)
    {
        _bt_unlockbuf                   // 解S锁
        _bt_lockbuf(BT_WRITE)           // 加X锁
        _bt_moveright(BT_WRITE)         // 对根页解锁后、加锁前,可能已经分裂,若分裂则需要右移到右兄弟页
    }
}

可以看到,搜索的流程非常简单,对路径上的节点都上的S锁,BT_WRITE方式则对叶子节点上X锁,BT_READ方式则对叶子节点上S锁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
_bt_moveright(
    nextkey // false则搜索第一个>= scankey的记录,true则搜索第一个> scankey的记录
    forupdate // true则尝试修复遇到的未完成的分裂
    stack // 仅用于修复未完成的分裂
    access // READ或WRITE
)
{
    // nextkey = false:如果scankey > high key,则页已分裂,需要右移(若scankey等于high key,可能需要右移,需要扫描右页)
    // nextkey = true:如果scankey >= high key,则右移
    cmpval = nextkey ? 0 : 1

    for(;;)
    {
        if (已经是最右页)
            break;

        // 修复未完成的分裂
        if (forupdate && INCOMPLETE_SPLIT)
        {
            // access是READ模式,则buf是S锁,需要升级成X锁

            // 释放S锁、获取X锁有窗口,可能已经完成分裂
            if (INCOMPLETE_SPLIT)
                _bt_finish_split
            else
                _bt_relbuf // 释放X锁
            // 重新根据access获取锁,重新检查
            buf = _bt_getbuf
            continue
        }
        
        if (_bt_compare(scankey, P_HIKEY) >= cmpval)
        {
            // 如果scankey大于(或等于)页的high key,则右移,会释放当前页
            _bt_relandgetbuf(next)
            continue; // 可能右移多次
        }
        else
            break;  // 结束
    }
}