【PGCCC】Postgres MVCC 內部:更新與插入的隱性成本

為什么 Postgres 中的更新操作有時感覺比插入操作慢?答案在于 Postgres 如何在后臺管理數據版本。
Postgres 高效處理并發事務能力的核心是多版本并發控制(MVCC)。
在本文中,我將探討 MVCC 在 Postgres 中的工作原理以及它如何影響寫入和讀取,以及插入和更新之間的性能差異。

-- pageinspect 擴展用于查看底層數據存儲,我們很快就會用到它!
CREATE EXTENSION IF NOT  EXISTS pageinspect; 
-- 創建表存儲
CREATE  TABLE store (     id SERIAL PRIMARY KEY,     name TEXT NOT  NULL ,value  INT  NOT  NULL ); 
-- 禁用表的自動清理
ALTER  TABLE store SET (autovacuum_enabled =  false );

設置過程包括創建一個名為“store”的示例表并禁用自動清理。這將使我們能夠觀察 Postgres 如何在沒有自動清理的情況下處理版本控制(MVCC 的實際操作),從而幫助我們更好地理解 MVCC 的工作原理。

MVCC概述

為了理解為什么更新行為與插入不同,我們首先看看 MVCC 在 Postgres 中的工作方式。
多版本并發控制 (MVCC) 是 Postgres 用于處理同一行的并發事務同時保持數據一致性和隔離性的機制。
Postgres 不會在讀取和寫入期間鎖定行,而是創建一行的多個版本,以允許事務在不相互阻塞的情況下運行。
一行的多個版本被稱為元組 (Tuples)。如果插入一行,則有 1 個元組與之關聯。如果更新同一行,則存在 2 個元組,其中一個是活的,另一個是死的。
工作原理如下:

寫入操作

每個插入或更新查詢都在一個事務中執行,每個事務都會被分配一個唯一的事務 ID。雖然該過程還涉及寫入預寫日志 (WAL) 和設置檢查點,但本文不會介紹這些細節。
您可以通過以下方式檢查postgres中的當前事務ID:

postgres = # SELECT txid_current(); 
txid_current 
--------------           
753 
( 1)postgres = # SELECT txid_current();
txid_current 
--------------           
754 ( 1)

Postgres 中的每一行都包含與事務 ID 綁定的版本信息,用于跟蹤該行隨時間的變化狀態。此版本控制通過 xmin 和 xmax 值進行管理,這些值可以直接查詢。

postgres = #插入 store (name, value )值( 'score' , 10 );插入 0  1 postgres = #從store中選擇xmin, xmax, * 其中id = 1 ; xmin | xmax | id | name   | value ------+------+----+-------+------- 755 | 0 | 1 | score | 10 ( 1行)
  • min表示插入該行(即創建初始元組)的事務的 ID。
  • xmax表示刪除或使該行失效的事務的 ID。由于這里的 xmax 為 0,因此表示該行處于活動狀態,未被刪除或失效。

Read Operation這些值也會影響行可見性,我們將在下面的文章中更詳細地探討。
讓我們看一下數據庫中存儲的實際元組,看看 MVCC 內部是如何管理行版本的。我們可以使用 pageinspect 擴展直接檢查元組,這使我們能夠查看表底層頁面的原始內容。
Postgres 以頁的形式將數據存儲在磁盤上,每個頁包含多個元組。理想情況下,我們感興趣的元組應該位于第一頁(即第 0 頁)。讓我們使用 pageinspect 來查詢它,看看存儲了什么:

postgres = #從heap_page_items(get_raw_page( 'store' , 0 ))中選擇lp、t_ctid、t_xmin、t_xmax ; lp | t_ctid | t_xmin | t_xmax ----+--------+--------+-------- 1 | ( 0 , 1 )   | 755 | 0 ( 1行)

我們可以看到 xmin 和 xmax 值與表上的常規 SELECT 查詢的值匹配。

  • lp– 這是行指針,充當頁面內的索引,指向元組的實際位置。由于lp = 1,這意味著這是頁面中第一個且唯一的元組。
  • t_ctid– 這是元組的 ID,以 (page_number, tuple_number)格式指向元組的物理位置。如果該行被更新,則會創建一個新版本,并且 ctid 將指向新的位置——稍后會詳細介紹。
  • xmin–表示插入該行的事務 ID(在本例中為事務 755)。
  • xmax– 表示刪除或使該行失效的事務 ID。由于 xmax = 0,因此該行仍然有效。 Postgres 中的元組由系統列(例如 xmin、xmax、ctid 等)和實際行數據組成。系統列幫助 Postgres 管理行版本和可見性,稍后我們將更詳細地探討這些內容。

當發生寫入操作(例如更新)時,現有行不會被直接修改。相反,Postgres 會創建一個包含更新值的新行版本,并保持舊行不變。然后,舊元組會被標記為“已死”,但仍可用于 MVCC 用途(例如支持并發讀取)。
讓我們更新行并檢查更改:

postgres = # UPDATE store set  value = 20  where id = 1 ; 
UPDATE  1 postgres = # SELECT xmin, xmax, *  FROM store WHERE id = 1 ; 
xmin | xmax | id | name   |  value 
------+------+----+-------+-------   
756  |     0  |   1  | score |     20 
( 1)

該行現在有了一個新的 xmin 值 (756),它代表創建此新元組的事務 ID。由于 MVCC 機制,我們預期原始行(現在是一個死元組)仍然與新行并存。
我們可以通過使用視圖檢查活元組和死元組的數量來確認這一點pg_stat_all_tables:

postgres = #從pg_stat_all_tables中選擇n_live_tup、n_dead_tup、relname ,其中relname = 'store';n_live_tup | n_dead_tup | relname ------------+------------+--------- 1 | 1 | store (1行) 

正如預期的那樣,有 1 個活動元組(更新后的行)和 1 個死亡元組(原始行)。現在讓我們檢查底層頁面以查看這兩個元組:
讓我們查詢頁面來再次查看元組。


postgres = #從heap_page_items(get_raw_page( 'store' , 0 ))中選擇lp、t_ctid、t_xmin、t_xmax ; lp | t_ctid | t_xmin | t_xmax ----+--------+--------+-------- 1 | ( 0 , 2 )   | 755 | 756 2 | ( 0 , 2 )   | 756 | 0 ( 2行)

我們現在看到了預期的兩個元組:

  • 第一個元組有 xmin = 755(來自原始插入)和 xmax = 756,表明它被事務 756(更新)無效。
  • 第二個元組是更新創建的新版本,其中 xmin = 756 和 xmax = 0(表示它仍然有效)。
  • 請注意,t_ctid原始元組的 已從更改為(0,1)。(0,2)這是因為t_ctid指向該行的最新版本。該格式(page_number,line_pointer)表示該行的最新版本位于第 0 頁,行指針為 2。

Heap Only Tuples (HOT)

此行為是 Postgres 一項名為“僅堆元組 (HOT)”的優化的一部分。在執行 UPDATE 操作時,Postgres 不會立即更新索引以指向新元組(這樣做成本較高),而是會更新舊元組的 t_ctid 以指向新版本。這會創建一個元組鏈——索引指向舊元組,舊元組指向新元組,依此類推。
Postgres 依靠 Vacuum 進程來清理死元組,并在稍后更新索引。
您可以在 Postgres 官方文檔中了解更多關于 HOT 的信息:

👉 https://www.postgresql.org/docs/current/storage-hot.html

Vacuum Process

你可能已經注意到,在之前的例子中,vacuum 操作被禁用了。這是故意為之,因為它允許我們觀察死元組和 HOT 鏈,否則它們會被vacuum 進程清理掉。
真空在 Postgres 中扮演著至關重要的角色,它可以永久刪除死元組、回收存儲空間,并更新索引以消除熱鏈——所有這些都有助于提升讀取性能。它還可以通過釋放死元組占用的存儲空間來防止表膨脹。
清理通常配置為自動運行,但也可以手動運行。讓我們嘗試清理包含 1 個死元組的表。

postgres = # SELECT n_live_tup, n_dead_tup, relname FROM pg_stat_all_tables WHERE relname =  'store' ; 
n_live_tup | n_dead_tup | relname 
------------+------------+---------           
1  |           1  | store ( 1  row ) 
--- 我們有 1 個死元組
--- 手動清理“store”表
postgres = # VACUUM store; 
VACUUM 
--- 我們最終得到 0 個死元組
postgres = # SELECT n_live_tup, n_dead_tup, relname FROM pg_stat_all_tables WHERE relname =  'store' ; 
n_live_tup | n_dead_tup | relname 
------------+------------+---------           
1  |           0  |存儲
(1 行)
postgres = #從heap_page_items(get_raw_page( 'store',0 ))中選擇lp、t_ctid、t_xmin、t_xmax ; lp | t_ctid | t_xmin | t_xmax ----+--------+--------+ -------- 1 | | | 2 |(0,2)| 756 | 0 ( 2  行)
  • 檢查頁面時我們只能看到一個元組,位于 lp 2,因為lp1 處的舊元組已被刪除。

然而,清理表是有代價的。它會消耗 CPU 和內存,占用原本可以用于讀寫的資源,從而造成性能瓶頸。此外,清理所需的時間可能從幾秒到幾小時不等,具體取決于所需的清理量。

真空對于 至關重要Transaction Wraparound。您可以在這里相關信息

https://www.postgresql.org/docs/current/routine-vacuuming.html#VACUUM-FOR-WRAPAROUND

對讀取性能的影響

MVCC 不僅影響寫入性能,還會影響讀取性能。執行查詢時,Postgres 需要確定哪個行版本有效,這會增加一些開銷:

1. 行可見性檢查

每次讀取一行時,Postgres 都會檢查其xmin和xmax值以確定它是否是最新版本。這會增加一些處理時間,尤其是在由于頻繁更新而存在多個行版本的情況下。

2. 表膨脹

當更新操作創建新的行版本時,舊版本將保留在表中,直到VACUUM操作將其刪除。隨著時間的推移,這會增加表的大小并降低讀取速度,因為 Postgres 可能需要掃描多個行版本才能找到有效的版本。
頻繁VACUUM并AUTOVACUUM有助于減少膨脹并提高讀取性能。

3. 寫入密集型工作負載中的更新與插入

讀取延遲可能會有所不同,具體取決于您是更新現有行還是插入新行:

  • 插入——直接添加新行而不影響現有行。
  • 更新——創建一個新的行版本并將舊版本標記為死版本,這會增加表的大小,并且需要在讀取期間做更多的工作才能找到最新版本。

對于更新,Postgres 可能需要調整索引指針,因為每次更新都會創建一個新的元組。但是,如果更新符合HOT(僅堆元組)更新的條件,則意味著索引不需要立即更新,從而提高效率。
然而,熱更新會在堆中創建元組鏈,這會增加的工作量VACUUM。Postgres 需要在讀取期間跟蹤鏈以查找最新的有效版本,并且一旦VACUUM刪除舊版本,索引可能仍需要調整以反映最新狀態。
頻繁執行自動清理有助于清理死行并保持一致的性能。如果您在寫入密集型工作負載中發現讀取速度變慢,降低更新頻率或調整數據模型可能會有所幫助。
這并不是建議你完全避免更新。但是,如果你在寫入密集型工作負載下遇到更高的讀取延遲,則值得考慮高更新頻率是否是導致此問題的原因。優化數據模型以減少或避免過度更新,可以幫助緩解此問題并提高整體性能。
對于寫入極其繁重的情況,針對高寫入吞吐量進行優化的寬列存儲(如 Cassandra 或 ScyllaDB)可以提供更好的性能。

對存儲空間的影響

由于 MVCC,我們進行的更新越多,Postgres 消耗的磁盤空間就越多,而vacuum 負責回收該空間。
讓我們用一個例子來演示一下,我將插入 100 萬條記錄并執行一些更新。我們還將查看每一步表占用的存儲空間。

postgres = #截斷store; 
TRUNCATE  TABLE --- 
插入一百萬條記錄postgres = # DO $$ 
BEGINFOR i IN  1. .1000000 LOOPINSERT  INTO store (name, value )VALUES ( 'Name_'  || i, i *  10 );     END LOOP; 
END $$; 
DO 
postgres = # SELECT  COUNT ( * ) FROM store;count ---------1000000 ( 1  row )

讓我們看看這個表占用的空間。

postgres = # SELECTrelname AS table_name,     pg_size_pretty(pg_total_relation_size(relid)) AS total_size 
FROMpg_catalog.pg_statio_user_tables 
WHERErelname =  'store' ; table_name | total_size------------+------------ store       |  71 MB ( 1)

這 100 萬條記錄占用了大約 71 MB 的磁盤空間。讓我們更新所有行并檢查存儲空間(我們仍然禁用了自動清理功能)。

postgres = #更新存儲設置 值= 2000 ;
更新 1000000 postgres = # SELECT     relname AS table_name,     pg_size_pretty(pg_total_relation_size(relid)) AS total_size FROMpg_catalog.pg_statio_user_tables 
WHERErelname =  'store' ; 
table_name | total_size 
------------+------------ 
store       |  142 MB ( 1)

正如預期的那樣,由于更新語句之后每行都有 2 個元組(1 個死元組和 1 個活元組),因此表的大小增加了一倍。
讓我們來看看這張表占用的空間。


postgres = # VACUUM 存儲;
VACUUM postgres = # SELECTrelname AS table_name,    pg_size_pretty(pg_total_relation_size(relid)) AS total_size
FROMpg_catalog.pg_statio_user_tables 
WHERE     relname =  'store' ; table_name | total_size 
------------+------------
存儲      |  142 MB 
(1 行)
postgres = # SELECT n_live_tup,n_dead_tup,relname FROM pg_stat_all_tables WHERE relname =  'store' ; 
n_live_tup | n_dead_tup | relname 
------------+------------+---------1000000  |           0  |存儲
(1 行)

有趣的是,在清理表之后,存儲空間仍然為 142 MB,但死元組的數量已降至零,證實清理操作已成功清理元組。
這是 Vacuum 的預期行為——它不會減少總存儲空間,而是將釋放的空間標記為可重用。這意味著任何新數據都將寫入現有存儲空間,而不是增加整體磁盤使用率。
但是,在某些情況下,這可能并不理想。如果您真的想回收磁盤空間,可以運行 VACUUM FULL 操作:


postgres = # VACUUM FULL存儲;
VACUUM postgres = # SELECTrelname AS table_name,   pg_size_pretty(pg_total_relation_size(relid)) AS total_size 
FROMpg_catalog.pg_statio_user_tables
WHERErelname =  'store';
table_name | total_size 
------------+------------ 
store       |  71 MB 
(1 行)

在這個例子中,VACUUM FULL 回收了之前標記為可重復使用或未使用的物理磁盤空間,從而減少了整體存儲大小。

結論

我們詳細介紹了 MVCC 的工作原理以及真空過程對性能的重要性。
這篇文章中還有很多內容我沒有涉及,我鼓勵您閱讀其中的一些主題(或者除非我決定在將來的某個時候撰寫它們):

  • 真空分析操作
  • 重建索引操作
  • 交易回溯調節
  • 自動真空

要點:MVCC 允許 Postgres 高效地處理并發事務,但也帶來了一些弊端。插入操作通常比更新操作讀取/性能更快,因為更新操作會創建新的行版本,這會導致數據庫膨脹,除非通過清理操作進行妥善管理。了解 MVCC 和 HOT 有助于您微調數據庫以獲得更佳性能。
#PG證書#PG考試#PostgreSQL培訓#PostgreSQL考試#PostgreSQL認證

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/bicheng/77868.shtml
繁體地址,請注明出處:http://hk.pswp.cn/bicheng/77868.shtml
英文地址,請注明出處:http://en.pswp.cn/bicheng/77868.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

Docker使用、容器遷移

Docker 簡介 Docker 是一個開源的容器化平臺,用于打包、部署和運行應用程序及其依賴環境。Docker 容器是輕量級的虛擬化單元,運行在宿主機操作系統上,通過隔離機制(如命名空間和控制組)確保應用運行環境的一致性和可移…

c#清理釋放內存

雖然c#具有內存管理和垃圾回收機制,但是在arcobjects二次開發嵌入到arcgis data reviewet還會報內存錯誤。需要強制清理某變量內存方法如下: 1設置靜態函數ReleaseCom函數 public static void ReleaseCom(object o) { try{System.Runtime.InteropServices.Marsh…

Linux:進程:進程控制

進程創建 在Linux中我們使用fork函數創建新進程: fork函數 fork函數是Linux中的一個系統調用,用于創建一個新的進程,創建的新進程是原來進程的子進程 返回值:如果子進程創建失敗,返回值是-1。如果子進程創建成功&a…

day1-小白學習JAVA---JDK安裝和環境變量配置(mac版)

JDK安裝和環境變量配置 我的電腦系統一、下載JDK1、oracle官網下載適合的JDK安裝包,選擇Mac OS對應的版本。 二、安裝三、配置環境變量1、終端輸入/usr/libexec/java_home -V查詢所在的路徑,復制備用2、輸入ls -a3、檢查文件目錄中是否有.bash_profile文…

Python項目--基于機器學習的股票預測分析系統

1. 項目介紹 在當今數字化時代,金融市場的數據分析和預測已經成為投資決策的重要依據。本文將詳細介紹一個基于Python的股票預測分析系統,該系統利用機器學習算法對歷史股票數據進行分析,并預測未來股票價格走勢,為投資者提供決策…

計算機視覺與深度學習 | 基于YOLOv8與光流法的目標檢測與跟蹤(Python代碼)

===================================================== github:https://github.com/MichaelBeechan CSDN:https://blog.csdn.net/u011344545 ===================================================== 目標檢測與跟蹤 關鍵實現邏輯檢測-跟蹤協作機制?特征點選擇策略?運動…

Java集合及面試題學習

知識來源沉默王二、小林coding、javaguide 1、ArrayList list.add("66") list.get(2) list.remove(1) list.set(1,"55") List<String> listnew ArrayList<>(); 底層是動態數組 添加元素流程&#xff1a;判斷是否擴容&#xf…

OSPF --- LSA

文章目錄 一、OSPF LSA&#xff08;鏈路狀態通告&#xff09;詳解1. LSA通用頭部2. OSPFv2 主要LSA類型a. Type 1 - Router LSAb. Type 2 - Network LSAc. Type 3 - Summary LSAd. Type 4 - ASBR Summary LSAe. Type 5 - AS External LSAf. Type 7 - NSSA External LSA 3. LSA泛…

Spring Boot 框架介紹及 Spring Boot 與 Spring 實現對比

在日常 Java Web 開發中&#xff0c;Spring 框架幾乎是繞不開的技術體系。傳統的 Spring 項目因其靈活強大而被廣泛應用&#xff0c;但隨著項目規模擴大與業務復雜度提升&#xff0c;XML 配置繁瑣、部署復雜等問題逐漸顯現。為此&#xff0c;Spring Boot 應運而生。 Spring Boo…

基于CNN卷積神經網絡和GEI步態能量提取的視頻人物步態識別算法matlab仿真

目錄 1.算法運行效果圖預覽 2.算法運行軟件版本 3.部分核心程序 4.算法理論概述 4.1 GEI步態能量提取 4.2 CNN卷積神經網絡原理 5.算法完整程序工程 1.算法運行效果圖預覽 (完整程序運行后無水印) 2.算法運行軟件版本 matlab2024b/matlab2022a 3.部分核心程序 &…

創建型模式:建造者模式

什么是建造者模式 建造者模式&#xff08;Builder Pattern&#xff09;是一種創建型設計模式&#xff0c;它將一個復雜對象的構建過程與其表示分離&#xff0c;使得同樣的構建過程可以創建不同的表示。簡單來說&#xff0c;建造者模式允許您一步一步創建復雜對象&#xff0c;而…

Linux `init 5` 相關命令的完整使用指南

Linux init 5 相關命令的完整使用指南—目錄 一、init 系統簡介二、init 5 的含義與作用三、不同 Init 系統下的 init 5 行為1. SysVinit&#xff08;如 CentOS 6、Debian 7&#xff09;2. systemd&#xff08;如 CentOS 7、Ubuntu 16.04&#xff09;3. Upstart&#xff08;如 …

RabbitMQ常見面試題回答重點

文章目錄 什么是消息隊列&#xff1f;為什么需要消息隊列消息隊列的模型消息隊列常見名詞如何保證消息不丟失&#xff1f;&#xff08;可靠性&#xff09;如何保證消息不重復/業務冪等性如何保證消息有序性如何處理消息堆積消息隊列設計為推送還是拉取 / 推拉模式優點無法路由的…

欣佰特攜數十款機器人相關前沿產品,亮相第二屆人形機器人和具身智能行業盛會

2025年4月15日至16日&#xff0c;備受關注的第二屆中國人形機器人與具身智能產業大會已在北京成功舉行。作為國內前沿科技及產品服務領域的重要參與者&#xff0c;欣佰特科技攜眾多前沿產品精彩亮相&#xff0c;全方位展示了其在人形機器人與具身智能領域的創新產品。 在本次大…

Docker安裝 (centos)

1.安裝依賴包&#xff1a; sudo yum install -y yum-utils device-mapper-persistent-data lvm2 2.刪除已有的 Docker 倉庫文件&#xff08;如果有&#xff09;&#xff1a; sudo rm -f /etc/yum.repos.d/docker-ce.repo 3.添加阿里云的 Docker 倉庫&#xff1a; sudo yum…

Vue接口平臺學習十——接口用例頁面2

效果圖及簡單說明 左邊選擇用例&#xff0c;右側就顯示該用例的詳細信息。 使用el-collapse折疊組件&#xff0c;將請求到的用例詳情數據展示到頁面中。 所有數據內容&#xff0c;綁定到caseData中 // 頁面綁定的用例編輯數據 const caseData reactive({title: "",…

服務器數據遷移指南

服務器數據遷移是將數據從一臺服務器轉移到另一臺服務器的過程&#xff0c;可能是為了硬件升級、云遷移、數據中心搬遷或服務整合。 以下是數據遷移的關鍵步驟和注意事項&#xff1a; 遷移前準備 制定遷移計劃 確定遷移范圍(全量/增量) 評估數據量和網絡帶寬 制定時間表和回…

25.解決中醫知識問答刪除歷史對話功能后端處理請求時拋出異常

ChatTest.vue:176 DELETE http://localhost:8080/api/chat/conversations/20 500 (Internal Server Error) deleteConversation ChatTest.vue:176 onClick ChatTest.vue:22 ChatTest.vue:185 刪除失敗 AxiosError {message: Request failed with status code 500, name: Axio…

記錄seatunnel排查重復數據的案例分析

文章目錄 背景分析檢查現象檢查B集群是否有異常&#xff0c;導致重復消費的分析同步任務 修復問題發現flink job 一直報異常修復問題 背景 使用seatunnel 同步數據從A 集群kafka 同步到B集群kafka,現象是發現兩邊數據不一致&#xff0c;每天10w級別會多幾十條數據 分析 檢查…

VSCode遠程圖形化GDB

VSCode遠程圖形化GDB 摘要一、安裝VSCode1、使用.exe安裝包安裝VSCode2、VSCode 插件安裝3、VSCode建立遠程連接 二、core dump找bug1、開啟core文件2、永久生效的方法3、編寫測試程序4、運行結果5、查看core段錯誤位置6、在程序中開啟core dump并二者core文件大小 三、gdbserv…