網站首頁 編程語言 正文
1、HOT概述
PostgreSQL中,由于其多版本的特性,當我們進行數據更新時,實際上并不是直接修改元數據,而是通過新插入一行數據來進行間接的更新。而當表上存在索引時,由于新插入了數據,那么索引必然也需要同步進行更新,這在索引較多的情況下,對于更新的性能影響必然很大。
為了解決這一問題,pg從8.3版本開始就引入了HOT(Heap Only Tuple)機制。其原理大致為,當更新的不是索引字段時,我們通過將舊元組指向新元組,而原先的索引不變,仍然指向舊元組,但是我們可以通過舊元組作為間接去訪問到新的元組,這樣就不用再去更新索引了。
2、HOT實現技術細節
要使用HOT進行更新,需要滿足兩個前提:
- 新的元組和舊元組必須在同一個page中;
- 索引字段不能進行更新。
當我們進行HOT更新時,首先是分別設置舊元組的t_informask2標志位為HEAP_HOT_UPDATED和新元組為HEAP_ONLY_TUPLE。
更新如下圖所示:
我們更新tuple1為tuple2,分別設置這兩行元組的t_informask2標志位,然后tuple1的ctid指向tuple2,而tuple2指向自己。
但是這樣存在一個明顯的問題,我們都知道pg會定期進行vacuum清理那些死元組,那么我們這里如果通過tuple1去訪問tuple2的話,tuple1這個死元組被清理了又該怎么辦呢?
所以pg會在合適的時機進行行指針的重定向,將舊元組的行指針指向新元組的行指針,這一過程稱為修剪。于是在修剪之后,我們通過HOT機制訪問數據便成了這樣:
1、通過索引元組找到舊元組的行指針1;
2、通過重定向的行指針1找到行指針2;
3、通過行指針2找到新元組tuple2。
這樣即使舊元組tuple1被清理掉也沒有影響了。
HOT對應的wal日志實現:
對于HOT的update操作,其wal日志中記錄的信息主要是由xl_heap_update結構存儲。
如果新的元組存儲在 block_id 為 0 的塊上,如果不是 XLOG_HEAP_HOT_UPDATE,那么舊的元組將會存儲在 block_id 為 1 的塊上。反之如果block_id 為 1 的塊沒有被使用,那么則認為是 XLOG_HEAP_HOT_UPDATE。
3、何時進行修剪
前面我們提到了,舊行的行指針會重定向到新行的行指針,這一過程稱之為修剪。那么什么時候會發生修剪呢?
一般來說,當我們執行select、update、insert、delete這些命令時均有可能觸發修剪,其觸發機制大致有兩種情況:
- 上一次進行update時無法在本page找到足夠的空間;
- 當前page上剩余空間小于fill-factor的值,最多小于10%
除此之外,當進行修剪時,還會選擇合適的時機進行死元組的清理,這一操作稱為碎片整理。碎片整理發生在當我們對元組進行檢索時發現空閑空間少于10%時,和修剪不同的是,碎片整理不會發生在insert時,因為該操作并不會檢索行。
相較于普通的vacuum操作,碎片清理并不涉及索引元組的清理,開銷相對于常規的清理要小很多,是通過PageRepairFragmentation函數來實現的。
這也是為什么HOT要求新舊元組需要在同一個page中,雖然從理論上來說我們可以將行指針的鏈表指向不同page,但是這樣我們便不能使用page-local的操作來進行碎片清理了。
4、HOT的不足
前面我們提到了HOT的前提條件之一就是:更新的列不能是索引列。需要注意,當更新的列是索引列時并不僅僅是會修改該列上的索引,整張表上所有的索引均會被修改。
例子:
創建測試表:
bill=# create table a(id int, c1 int, c2 int, c3 int);
CREATE TABLE
bill=# insert into a select generate_series(1,10), random()*100, random()*100, random()*100;
INSERT 0 10
bill=# create index idx_a_1 on a (id);
CREATE INDEX
bill=# create index idx_a_2 on a (c1);
CREATE INDEX
bill=# create index idx_a_3 on a (c2);
CREATE INDEX
觀察索引頁內容:
bill=# SELECT * FROM bt_page_items('idx_a_1',1);
itemoffset | ctid | itemlen | nulls | vars | data | dead | htid | tids
------------+--------+---------+-------+------+-------------------------+------+--------+------
1 | (0,1) | 16 | f | f | 01 00 00 00 00 00 00 00 | f | (0,1) |
2 | (0,2) | 16 | f | f | 02 00 00 00 00 00 00 00 | f | (0,2) |
3 | (0,3) | 16 | f | f | 03 00 00 00 00 00 00 00 | f | (0,3) |
4 | (0,4) | 16 | f | f | 04 00 00 00 00 00 00 00 | f | (0,4) |
5 | (0,5) | 16 | f | f | 05 00 00 00 00 00 00 00 | f | (0,5) |
6 | (0,6) | 16 | f | f | 06 00 00 00 00 00 00 00 | f | (0,6) |
7 | (0,7) | 16 | f | f | 07 00 00 00 00 00 00 00 | f | (0,7) |
8 | (0,8) | 16 | f | f | 08 00 00 00 00 00 00 00 | f | (0,8) |
9 | (0,9) | 16 | f | f | 09 00 00 00 00 00 00 00 | f | (0,9) |
10 | (0,10) | 16 | f | f | 0a 00 00 00 00 00 00 00 | f | (0,10) |
(10 rows)
bill=# SELECT * FROM bt_page_items('idx_a_2',1);
itemoffset | ctid | itemlen | nulls | vars | data | dead | htid | tids
------------+-----------+---------+-------+------+-------------------------+------+--------+-------------------
1 | (0,5) | 16 | f | f | 00 00 00 00 00 00 00 00 | f | (0,5) |
2 | (0,4) | 16 | f | f | 05 00 00 00 00 00 00 00 | f | (0,4) |
3 | (0,8) | 16 | f | f | 0e 00 00 00 00 00 00 00 | f | (0,8) |
4 | (0,9) | 16 | f | f | 25 00 00 00 00 00 00 00 | f | (0,9) |
5 | (16,8194) | 32 | f | f | 30 00 00 00 00 00 00 00 | f | (0,1) | {"(0,1)","(0,3)"}
6 | (0,10) | 16 | f | f | 58 00 00 00 00 00 00 00 | f | (0,10) |
7 | (0,6) | 16 | f | f | 60 00 00 00 00 00 00 00 | f | (0,6) |
8 | (0,7) | 16 | f | f | 62 00 00 00 00 00 00 00 | f | (0,7) |
9 | (0,2) | 16 | f | f | 63 00 00 00 00 00 00 00 | f | (0,2) |
(9 rows)
bill=# SELECT * FROM bt_page_items('idx_a_3',1);
itemoffset | ctid | itemlen | nulls | vars | data | dead | htid | tids
------------+--------+---------+-------+------+-------------------------+------+--------+------
1 | (0,6) | 16 | f | f | 03 00 00 00 00 00 00 00 | f | (0,6) |
2 | (0,3) | 16 | f | f | 12 00 00 00 00 00 00 00 | f | (0,3) |
3 | (0,5) | 16 | f | f | 15 00 00 00 00 00 00 00 | f | (0,5) |
4 | (0,9) | 16 | f | f | 1a 00 00 00 00 00 00 00 | f | (0,9) |
5 | (0,4) | 16 | f | f | 33 00 00 00 00 00 00 00 | f | (0,4) |
6 | (0,7) | 16 | f | f | 3d 00 00 00 00 00 00 00 | f | (0,7) |
7 | (0,10) | 16 | f | f | 4e 00 00 00 00 00 00 00 | f | (0,10) |
8 | (0,1) | 16 | f | f | 4f 00 00 00 00 00 00 00 | f | (0,1) |
9 | (0,2) | 16 | f | f | 5c 00 00 00 00 00 00 00 | f | (0,2) |
10 | (0,8) | 16 | f | f | 5d 00 00 00 00 00 00 00 | f | (0,8) |
(10 rows)
更新索引列c2:
bill=# update a set c2=c2+1 where id=1 returning ctid,*;
ctid | id | c1 | c2 | c3
--------+----+----+----+----
(0,11) | 1 | 19 | 66 | 86
(1 row)
UPDATE 1
再觀察索引內容:
可以看到3個索引的索引頁均發生了變化。
bill=# SELECT * FROM bt_page_items('idx_a_1',1);
itemoffset | ctid | itemlen | nulls | vars | data | dead | htid | tids
------------+--------+---------+-------+------+-------------------------+------+--------+------
1 | (0,1) | 16 | f | f | 01 00 00 00 00 00 00 00 | f | (0,1) |
2 | (0,11) | 16 | f | f | 01 00 00 00 00 00 00 00 | f | (0,11) |
3 | (0,2) | 16 | f | f | 02 00 00 00 00 00 00 00 | f | (0,2) |
4 | (0,3) | 16 | f | f | 03 00 00 00 00 00 00 00 | f | (0,3) |
5 | (0,4) | 16 | f | f | 04 00 00 00 00 00 00 00 | f | (0,4) |
6 | (0,5) | 16 | f | f | 05 00 00 00 00 00 00 00 | f | (0,5) |
7 | (0,6) | 16 | f | f | 06 00 00 00 00 00 00 00 | f | (0,6) |
8 | (0,7) | 16 | f | f | 07 00 00 00 00 00 00 00 | f | (0,7) |
9 | (0,8) | 16 | f | f | 08 00 00 00 00 00 00 00 | f | (0,8) |
10 | (0,9) | 16 | f | f | 09 00 00 00 00 00 00 00 | f | (0,9) |
11 | (0,10) | 16 | f | f | 0a 00 00 00 00 00 00 00 | f | (0,10) |
(11 rows)
bill=# SELECT * FROM bt_page_items('idx_a_2',1);
itemoffset | ctid | itemlen | nulls | vars | data | dead | htid | tids
------------+--------+---------+-------+------+-------------------------+------+--------+------
1 | (0,6) | 16 | f | f | 04 00 00 00 00 00 00 00 | f | (0,6) |
2 | (0,9) | 16 | f | f | 0b 00 00 00 00 00 00 00 | f | (0,9) |
3 | (0,1) | 16 | f | f | 13 00 00 00 00 00 00 00 | f | (0,1) |
4 | (0,11) | 16 | f | f | 13 00 00 00 00 00 00 00 | f | (0,11) |
5 | (0,2) | 16 | f | f | 19 00 00 00 00 00 00 00 | f | (0,2) |
6 | (0,5) | 16 | f | f | 1d 00 00 00 00 00 00 00 | f | (0,5) |
7 | (0,8) | 16 | f | f | 1e 00 00 00 00 00 00 00 | f | (0,8) |
8 | (0,4) | 16 | f | f | 21 00 00 00 00 00 00 00 | f | (0,4) |
9 | (0,3) | 16 | f | f | 28 00 00 00 00 00 00 00 | f | (0,3) |
10 | (0,10) | 16 | f | f | 3a 00 00 00 00 00 00 00 | f | (0,10) |
11 | (0,7) | 16 | f | f | 4d 00 00 00 00 00 00 00 | f | (0,7) |
(11 rows)
bill=# SELECT * FROM bt_page_items('idx_a_3',1);
itemoffset | ctid | itemlen | nulls | vars | data | dead | htid | tids
------------+--------+---------+-------+------+-------------------------+------+--------+------
1 | (0,2) | 16 | f | f | 17 00 00 00 00 00 00 00 | f | (0,2) |
2 | (0,7) | 16 | f | f | 18 00 00 00 00 00 00 00 | f | (0,7) |
3 | (0,5) | 16 | f | f | 33 00 00 00 00 00 00 00 | f | (0,5) |
4 | (0,6) | 16 | f | f | 37 00 00 00 00 00 00 00 | f | (0,6) |
5 | (0,4) | 16 | f | f | 38 00 00 00 00 00 00 00 | f | (0,4) |
6 | (0,1) | 16 | f | f | 41 00 00 00 00 00 00 00 | f | (0,1) |
7 | (0,11) | 16 | f | f | 42 00 00 00 00 00 00 00 | f | (0,11) |
8 | (0,9) | 16 | f | f | 4d 00 00 00 00 00 00 00 | f | (0,9) |
9 | (0,8) | 16 | f | f | 58 00 00 00 00 00 00 00 | f | (0,8) |
10 | (0,3) | 16 | f | f | 62 00 00 00 00 00 00 00 | f | (0,3) |
11 | (0,10) | 16 | f | f | 63 00 00 00 00 00 00 00 | f | (0,10) |
(11 rows)
這也意味著當我們無法使用HOT更新時,每更新一條數據都會更新所有的索引,那么這個對性能的影響可想而知。而我們簡單想想其實并沒有這個必要,因為對于沒有被更新的索引列而言,只是ctid發生了變化,其索引列指向的值并沒有變化。那么有沒有辦法讓我們更新索引列時只修改該索引列的索引呢?PHOT應運而生。
5、PHOT概述
PHOT(Partial Heap Only Tuples),正如我們前面所說,PHOT是為了解決HOT更新索引列時會修改索引列的弊端。目前,PG中還不支持該功能,不過社區中已經有相關的討論了,預計可能會在PG15中和大家見面。
PHOT的設計思路主要是:通過在t_infomask2標志位新加入兩種類型HEAP_HOT_UPDATED 和HEAP_ONLY_TUPLE用來表示PHOT更新,類似于HOT。當我們對索引列進行修改時,通過一個PHOT的bitmap位圖來記錄哪些索引列被更新了,然后,當我們對索引列修改時,只需要修改這個bitmap位圖中的列即可。
#define HEAP_HOT_UPDATED 0x4000 /* tuple was HOT-updated */
#define HEAP_ONLY_TUPLE 0x8000 /* this is heap-only tuple */
6、PHOT實例
我們通過下面的例子來看看PHOT和HOT的不同之處。
構建環境:
CREATE TABLE test (a INT, b INT, c INT);
CREATE INDEX ON test (a);
CREATE INDEX ON test (b);
CREATE INDEX ON test (c);
INSERT INTO test VALUES (0, 0, 0);
UPDATE test SET a = 1, b = 1;
UPDATE test SET b = 2, c = 2;
UPDATE test SET a = 2;
UPDATE test SET a = 3, c = 3;
不使用PHOT:
可以看到,不使用PHOT的情況下,每次更新都會產生新的索引元組。
在這些更新之后,有 15 個索引元組、5 個行指針和 5 個堆元組。
test_a_idx 0 1 1 2 3
test_b_idx 0 1 2 2 2
test_c_idx 0 0 2 2 3
lp 1 2 3 4 5
heap (0,0,0) (1,1,0) (1,2,2) (2,2,2) (3,2,3)
PHOT:
在使用PHOT后,通過增加了一個bitmap位圖來記錄被更新的列。當執行完上述更新后,有 10 個索引元組、5 個行指針、5 個堆元組和 4 個位圖。
test_a_idx 0 1 2 3
test_b_idx 0 1 2
test_c_idx 0 2 3
lp 1 2 3 4 5
heap (0,0,0) (1,1,0) (1,2,2) (2,2,2) (3,2,3)
bitmap xx- -xx x-- x-x
而前面的例子我們使用PHOT更新又是什么結果呢?
可以看到下圖中,左邊是使用PHOT進行更新的,只是被更新的索引列發生了變化,而右邊非PHOT進行更新則是所有的索引列均發生變化。
性能測試:
通過簡單的pgbench測試,TPS 提高了約 34%,其中每個表都有 5 個額外的文本列和每列上的索引。有了額外的索引和列,理論上 TPS 的提升應該會更高。對于沒有表修改的短期 pgbench 運行,使用 PHOT 觀察到 TPS 增加了約 2%,表明常規 pgbench 運行沒有受到顯著影響。
總結
PostgreSQL使用HOT機制來避免因為其多版本特性導致的每次更新數據均需要修改索引的情況。而當前的HOT機制,對于索引列的更新仍然存在比較明顯的性能問題,因為所有的索引均需要發生修改,不過預計在PG15中,將會加入更為強大的PHOT功能,更新索引列再也不會影響其它索引了。
參考鏈接:
傳送門
傳送門
原文鏈接:https://foucus.blog.csdn.net/article/details/121336372
相關推薦
- 2022-06-14 Docker安裝運行SRS的過程記錄_docker
- 2022-07-13 Docker的安裝部署與優化
- 2022-09-10 Go語言中的IO操作及Flag包的用法_Golang
- 2022-12-03 C?++迭代器iterator在string中使用方法介紹_C 語言
- 2022-04-24 Python元素集合的列表切片_python
- 2022-06-18 C#多線程的ResetAbort()方法_C#教程
- 2022-04-01 LVS,Nginx,Haproxy的差異
- 2022-09-03 C++日期類(Date)實現的示例代碼_C 語言
- 最近更新
-
- window11 系統安裝 yarn
- 超詳細win安裝深度學習環境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優雅實現加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發現-Nac
- Spring Security之基于HttpR
- Redis 底層數據結構-簡單動態字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支