Postgresql之HOT技術簡析

myownstars發表於2016-07-09
背景:
PG實現了MVCC,但是沒有引入undo表空間,為實現多版本讀,所有的update操作都插入一個新版本行(delete-marked-old + insert-new),資料行的各個版本從老到新形成一個單向連結串列,即update chain。
pre-8.3,索引塊也是採用同樣的策略,即便update沒有更新任何索引列,舉例說明:
表t有兩個列(id , name),其中name上有索引 ind_name,現在表有2個資料行,分別稱為tuple1和tuple2
(1, 'a')
(0, 'b')
執行update t set id =id+ 1 where name = 'a'
現在表上有3個資料行
(1, 'a') ---> (2, 'a')  這個是update chain, (1, 'a')此時被標記為deleted,且其指向(3, 'a')
(0, 'b')
而索引ind_name同樣會新增一個entry,儘管沒有更新索引列name
('a', tuple1_pos)  --> ('a', tuple1_pos),第2個entry沒有存在的必要
('b', tuple2_pos)
這會導致索引佔用更多的空間,提升訪問代價

改進
從8.3開始,當update語句同時滿足下述兩個條件時,索引不會新增entry:
1 update沒有更新索引列;
2 表塊有足夠空間容納update產生的新版本行,即同一update chain上的所有版本必須位於同一個資料塊中。
同樣的例子,執行update t set id =id+ 1 where name = 'a'後
現在表上有3個資料行
(1, 'a') ---> (2, 'a')
(0, 'b')
而索引ind_name沒有改變
('a', tuple1_pos)
('b', tuple2_pos)

儘管資料行從(1, 'a')變成了(2, 'a'),但是ind_name的索引項仍然指向(1, 'a'),回表時透過遍歷update chain找到(2, 'a')

這種改進被稱為HOT(heap only tuple)

實現原理
PG資料塊由幾部分組成: 塊頭;行指標;資料行;塊尾
其中行指標指向對應行物理位置,其組成為(lp_offset, lp_flags, lp_len) = (15b, 2b, 15b)
lp_offset為元組在塊內偏移量,2^15=32768,因此PG的資料塊最大為32K;
lp_flags描述元組的狀態,有4個候選值:未使用;正常使用;hot重定向;死亡
lp_len為元組的長度
每個資料行都有一個行指標,當全表掃描時,會讀取塊內的所有行指標以獲取資料行。

資料行的頭部包含一些後設資料欄位,其中ctid標註該行物理位置(offset, len),和行指標有重複嫌疑,當形成update chain時,ctid指向的是連結串列中下一個版本的位置,依靠它才能完成遍歷。

同時,PG不允許有行連結出現,因為這樣會導致索引回表訪問時產生額外IO,所以當資料塊不足以容納一個update chain上的所有元素時,便會在其他資料塊新建一條update chain,相應的索引也會新增一個entry。
當不斷執行update t set id =id +1 where name ='a' 時,block1會被撐爆,最終會變成如下形式:
block 1
(1, 'a') ---> (2, 'a') ---> .......
(0, 'b')

block 2
(N, 'a') ---> (N + 1, 'a') 
此時索引會新增一個entry
('a', tuple1_pos)   -->  ('a', tuple3_pos)
('b', tuple2_pos)


延伸
innodb的索引元組結構為(索引列,主鍵),每次回表都透過主鍵掃描B+樹來定位資料塊,再借助頁目錄在塊內定位資料行物理位置。
這種方式的優點是資料行的物理位置發生改變且索引列值不變時,不需要更新索引,缺點是每次回表會多消耗2個IO(普通B+樹一般為3層)。
因此,當update沒有更新索引列時,相應的索引完全不受影響,不存在類似PG的煩惱。

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

相關文章