PG 12-2 B-Tree 索引 分析 分裂 level = 1

qiqihappy發表於2022-04-10

感謝大神的分享,如下,

http://blog.itpub.net/6906/

https://www.cnblogs.com/xueqiuqiu/articles/10994404.html


透過上面的分析,我們可以思考一下,一個root page能夠存多少個item

我們知道B+樹的基本資料結構,root page 就是B+樹的頂層。

在我們的實驗中,一個索引元組的大小為16,每個索引元組都有一個行指標,4位元組,頁大小為8192,頁頭24位元組,special空間為16,那麼理論上可以存(8192-24-16)/(16+4)=407個元組

來驗證一下,

postgres=# drop table if exists t_index;
DROP TABLE
postgres=# vacuum full;
VACUUM
postgres=# create table t_index (id int,c1 char(8),c2 varchar(16));      
CREATE TABLE
postgres=# alter table t_index add constraint pk_t_index primary key(id);
ALTER TABLE
postgres=# do $$
postgres$# begin
postgres$#    for i in 1..407 loop
postgres$#    insert into t_index (id, c1, c2) values (i, '#'||i||'#', '#'||i||'#');
postgres$#    end loop;
postgres$# end $$;
DO
postgres=#

我們插入407個元組

檢視一下索引表,如下

postgres=# select * from bt_metap('pk_t_index');       
 magic  | version | root | level | fastroot | fastlevel | oldest_xact | last_cleanup_num_tuples 
--------+---------+------+-------+----------+-----------+-------------+-------------------------
 340322 |       4 |    1 |     0 |        1 |         0 |           0 |                      -1
(1 row)
postgres=# select * from bt_page_stats('pk_t_index',1);       
 blkno | type | live_items | dead_items | avg_item_size | page_size | free_size | btpo_prev | btpo_next | btpo | btpo_flags 
-------+------+------------+------------+---------------+-----------+-----------+-----------+-----------+------+------------
     1 | l    |        407 |          0 |            16 |      8192 |         8 |         0 |         0 |    0 |          3
(1 row)
postgres=# select * from page_header(get_raw_page('pk_t_index',1)); 
    lsn    | checksum | flags | lower | upper | special | pagesize | version | prune_xid 
-----------+----------+-------+-------+-------+---------+----------+---------+-----------
 0/5E73F38 |        0 |     0 |  1652 |  1664 |    8176 |     8192 |       4 |         0
(1 row)

這個時候還沒有分層,這裡的索引有序性是透過行指標來實現的。

現在插入一條新資料

postgres=# insert into t_index values(408, '$$408', '##408##');
INSERT 0 1
postgres=#

檢視索引表

postgres=# select * from bt_metap('pk_t_index');                                          
 magic  | version | root | level | fastroot | fastlevel | oldest_xact | last_cleanup_num_tuples 
--------+---------+------+-------+----------+-----------+-------------+-------------------------
 340322 |       4 |    3 |     1 |        3 |         1 |           0 |                      -1
(1 row)
postgres=# select * from bt_page_stats('pk_t_index',1);              
 blkno | type | live_items | dead_items | avg_item_size | page_size | free_size | btpo_prev | btpo_next | btpo | btpo_flags 
-------+------+------------+------------+---------------+-----------+-----------+-----------+-----------+------+------------
     1 | l    |        367 |          0 |            16 |      8192 |       808 |         0 |         2 |    0 |          1
(1 row)
postgres=# select * from bt_page_stats('pk_t_index',2); 
 blkno | type | live_items | dead_items | avg_item_size | page_size | free_size | btpo_prev | btpo_next | btpo | btpo_flags 
-------+------+------------+------------+---------------+-----------+-----------+-----------+-----------+------+------------
     2 | l    |         42 |          0 |            16 |      8192 |      7308 |         1 |         0 |    0 |          1
(1 row)
postgres=# select * from bt_page_stats('pk_t_index',3); 
 blkno | type | live_items | dead_items | avg_item_size | page_size | free_size | btpo_prev | btpo_next | btpo | btpo_flags 
-------+------+------------+------------+---------------+-----------+-----------+-----------+-----------+------+------------
     3 | r    |          2 |          0 |            12 |      8192 |      8116 |         0 |         0 |    1 |          2
(1 row)
postgres=#

可以看到分層了,除root外,還有一層(level = 1),page 1 為左子樹(btpo_next = 2),page 2 為右子樹(btpo_prev=1),其中page 1 有367條記錄,page 2有42條記錄,總共409條元組,符合B+樹所有的索引都儲存在葉子節點的定義,這裡比408條多一條,下面會解釋多的一條是什麼

同樣的檢視一下metapage

如下

postgres=# select * from page_header(get_raw_page('pk_t_index',0)); 
    lsn    | checksum | flags | lower | upper | special | pagesize | version | prune_xid 
-----------+----------+-------+-------+-------+---------+----------+---------+-----------
 0/5E8CEF8 |        0 |     0 |    64 |  8176 |    8176 |     8192 |       4 |         0
(1 row)
postgres=#

可以發現,空間使用情況和分層之前沒有區別,當然其中資料是肯定發生了變化,這裡就不贅述了。

接下去,看一下root page

postgres=# select * from bt_page_items('pk_t_index',3);                
 itemoffset | ctid  | itemlen | nulls | vars |          data           
------------+-------+---------+-------+------+-------------------------
          1 | (1,0) |       8 | f     | f    | 
          2 | (2,1) |      16 | f     | f    | 6f 01 00 00 00 00 00 00
(2 rows)
postgres=# select * from bt_page_stats('pk_t_index',3);             
 blkno | type | live_items | dead_items | avg_item_size | page_size | free_size | btpo_prev | btpo_next | btpo | btpo_flags 
-------+------+------------+------------+---------------+-----------+-----------+-----------+-----------+------+------------
     3 | r    |          2 |          0 |            12 |      8192 |      8116 |         0 |         0 |    1 |          2
(1 row)
postgres=#

這裡root節點有兩個item,其中一個指向最左邊的葉節點,不需要儲存最小值,對於這一點,另一個指向右葉節點,需要儲存右節點的最小值,又稱為儲存右鏈路,這裡的值為367

對於這一點的理解,是這樣的,我們思考一下在本例中查詢一個索引id,因為本例只有兩個葉節點,那麼只需要判斷索引id是大於367,還是小於367,如果是小於367,那麼找左葉節點,如果大於367,找右葉節點,如果=367,根據citd知道,2號page的1號元組中儲存的ctid,也就是(2,53),如下

postgres=# select * from bt_page_items('pk_t_index',2) limit 10;                            
 itemoffset |  ctid  | itemlen | nulls | vars |          data           
------------+--------+---------+-------+------+-------------------------
          1 | (2,53) |      16 | f     | f    | 6f 01 00 00 00 00 00 00
          2 | (2,54) |      16 | f     | f    | 70 01 00 00 00 00 00 00
          3 | (2,55) |      16 | f     | f    | 71 01 00 00 00 00 00 00
          4 | (2,56) |      16 | f     | f    | 72 01 00 00 00 00 00 00
          5 | (2,57) |      16 | f     | f    | 73 01 00 00 00 00 00 00
          6 | (2,58) |      16 | f     | f    | 74 01 00 00 00 00 00 00
          7 | (2,59) |      16 | f     | f    | 75 01 00 00 00 00 00 00
          8 | (2,60) |      16 | f     | f    | 76 01 00 00 00 00 00 00
          9 | (2,61) |      16 | f     | f    | 77 01 00 00 00 00 00 00
         10 | (2,62) |      16 | f     | f    | 78 01 00 00 00 00 00 00
(10 rows)

然後在找堆表的2號page的53號元組,如下

postgres=# select * from heap_page_items(get_raw_page('t_index',2)) where t_ctid = '(2,53)'; 
 lp | lp_off | lp_flags | lp_len | t_xmin | t_xmax | t_field3 | t_ctid | t_infomask2 | t_infomask | t_hoff | t_bits | t_oid |                  
t_data                  
----+--------+----------+--------+--------+--------+----------+--------+-------------+------------+--------+--------+-------+------------------
------------------------
 53 |   5648 |        1 |     43 |    902 |      0 |      366 | (2,53) |           3 |       2306 |     24 |        |       | \x6f0100001323333
637232020200d2333363723
(1 row)
postgres=#

所以不用儲存最小值。

這裡不去對root page 的special空間進行檢視,

兩個關鍵欄位

btpo = 1 說明還沒有到最底層(最底層btpo=0, 這種頁裡面儲存的ctid才代表指向heap page的地址)

btpo_flags=2 說明這個頁是root page


再來看一下,

索引page 1

postgres=# select * from bt_page_stats('pk_t_index',1);                                      
 blkno | type | live_items | dead_items | avg_item_size | page_size | free_size | btpo_prev | btpo_next | btpo | btpo_flags 
-------+------+------------+------------+---------------+-----------+-----------+-----------+-----------+------+------------
     1 | l    |        367 |          0 |            16 |      8192 |       808 |         0 |         2 |    0 |          1
(1 row)
postgres=# select * from bt_page_items('pk_t_index',1) limit 10;                             
 itemoffset | ctid  | itemlen | nulls | vars |          data           
------------+-------+---------+-------+------+-------------------------
          1 | (2,1) |      16 | f     | f    | 6f 01 00 00 00 00 00 00
          2 | (0,1) |      16 | f     | f    | 01 00 00 00 00 00 00 00
          3 | (0,2) |      16 | f     | f    | 02 00 00 00 00 00 00 00
          4 | (0,3) |      16 | f     | f    | 03 00 00 00 00 00 00 00
          5 | (0,4) |      16 | f     | f    | 04 00 00 00 00 00 00 00
          6 | (0,5) |      16 | f     | f    | 05 00 00 00 00 00 00 00
          7 | (0,6) |      16 | f     | f    | 06 00 00 00 00 00 00 00
          8 | (0,7) |      16 | f     | f    | 07 00 00 00 00 00 00 00
          9 | (0,8) |      16 | f     | f    | 08 00 00 00 00 00 00 00
         10 | (0,9) |      16 | f     | f    | 09 00 00 00 00 00 00 00
(10 rows)
postgres=# 
postgres=# select count(*) from bt_page_items('pk_t_index',1);            
 count 
-------
   367
(1 row)
postgres=#

這裡可以看到,page 1 有右兄弟節點,所以它的第一條item指向右兄弟節點的第一個元組,這符合B+樹的設計,葉節點和banch節點是一個雙向連結串列,其實我們進一步確定,如下

postgres=# select * from bt_page_items('pk_t_index',1) where ctid='(2,1)';                        
 itemoffset | ctid  | itemlen | nulls | vars |          data           
------------+-------+---------+-------+------+-------------------------
          1 | (2,1) |      16 | f     | f    | 6f 01 00 00 00 00 00 00
        316 | (2,1) |      16 | f     | f    | 3b 01 00 00 00 00 00 00
(2 rows)
postgres=#

可以看到,page 1 的ctid為(2,1)的記錄有兩條,進一步檢查

postgres=# select * from heap_page_items(get_raw_page('t_index',2)) where t_ctid = '(2,1)';            
 lp | lp_off | lp_flags | lp_len | t_xmin | t_xmax | t_field3 | t_ctid | t_infomask2 | t_infomask | t_hoff | t_bits | t_oid |                  
t_data                  
----+--------+----------+--------+--------+--------+----------+--------+-------------+------------+--------+--------+-------+------------------
------------------------
  1 |   8144 |        1 |     43 |    902 |      0 |      314 | (2,1)  |           3 |       2306 |     24 |        |       | \x3b0100001323333
135232020200d2333313523
(1 row)
postgres=# select * from bt_page_items('pk_t_index',2) limit 5;  
 itemoffset |  ctid  | itemlen | nulls | vars |          data           
------------+--------+---------+-------+------+-------------------------
          1 | (2,53) |      16 | f     | f    | 6f 01 00 00 00 00 00 00
          2 | (2,54) |      16 | f     | f    | 70 01 00 00 00 00 00 00
          3 | (2,55) |      16 | f     | f    | 71 01 00 00 00 00 00 00
          4 | (2,56) |      16 | f     | f    | 72 01 00 00 00 00 00 00
          5 | (2,57) |      16 | f     | f    | 73 01 00 00 00 00 00 00
(5 rows)
postgres=#

可以看到,的確page 1的第一個items指向的是page 2的第一個元組,這麼做的意義是,假設要查詢 id > 100的元組,那麼從root開始,走左子樹,找到data等於100的bt元組的,行指標,該行指標之後所有的元組都是大於100的(因為行指標是排好序的),那麼對於右兄弟節點上的所有資料,需要找到一個入口,這個入口就是1號item對應的地址。

再看一下,page 2

postgres=# select * from bt_page_stats('pk_t_index',2);                                                                            
 blkno | type | live_items | dead_items | avg_item_size | page_size | free_size | btpo_prev | btpo_next | btpo | btpo_flags 
-------+------+------------+------------+---------------+-----------+-----------+-----------+-----------+------+------------
     2 | l    |         42 |          0 |            16 |      8192 |      7308 |         1 |         0 |    0 |          1
(1 row)
postgres=# select * from bt_page_items('pk_t_index',2) limit 10;                               
 itemoffset |  ctid  | itemlen | nulls | vars |          data           
------------+--------+---------+-------+------+-------------------------
          1 | (2,53) |      16 | f     | f    | 6f 01 00 00 00 00 00 00
          2 | (2,54) |      16 | f     | f    | 70 01 00 00 00 00 00 00
          3 | (2,55) |      16 | f     | f    | 71 01 00 00 00 00 00 00
          4 | (2,56) |      16 | f     | f    | 72 01 00 00 00 00 00 00
          5 | (2,57) |      16 | f     | f    | 73 01 00 00 00 00 00 00
          6 | (2,58) |      16 | f     | f    | 74 01 00 00 00 00 00 00
          7 | (2,59) |      16 | f     | f    | 75 01 00 00 00 00 00 00
          8 | (2,60) |      16 | f     | f    | 76 01 00 00 00 00 00 00
          9 | (2,61) |      16 | f     | f    | 77 01 00 00 00 00 00 00
         10 | (2,62) |      16 | f     | f    | 78 01 00 00 00 00 00 00
(10 rows)

可以看到,page 2的所有item均為索引元組,其與左兄弟節點的關係,透過btpo_prev體現。

可以想象,如果,有3個葉節點,中間的節點,頁一定會有一個指向右兄弟節點的item

實際上上述的部落格中,都提到有,這裡不贅述了,可以參考上述部落格


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70012752/viewspace-2886389/,如需轉載,請註明出處,否則將追究法律責任。

相關文章