返回

文章详情

PostgreSQL的MVCC很糟糕。其他的也是。

Hacker News2026年7月29日 14:58

如果你关注那些不喜欢Postgres的人,首先你可能会了解到Postgres的MVCC很糟糕。四十年的设计错误。它的痕迹无处可见。膨胀的表,大小增加一倍,32位事务计数器限制,与VACUUM无穷无尽的斗争,以及死元组的噩梦。它还有一些背书:Uber在2016年测量了写放大效应,并因此转向MySQL;Andy Pavlo的数据库组称MVCC是他们最讨厌的PostgreSQL部分。这是一个真实的问题。Postgres的表现糟糕透顶。虽然这并没有夸大其词,但归根结底,这是一种真正的设计选择。膨胀、加剧的写入、VACUUM的照料:每一项费用都追溯到一个决策,而不是缺陷,我们将在下面的实时PostgreSQL 19 beta2实例上逐一复现,以便你可以亲眼目睹损害的发生。但传播于各个社区的评判总是早早停留在一个问题上:与什么相比?其他每个引擎又是如何做的,代价又是什么?因为MVCC并非可选。任何希望读者不阻塞写者的数据库都必须在某处保留行的多个版本,每个这样做的引擎都要回答同样的四个问题:旧版本存在哪里?在表中,还是在一个单独的结构中?版本链的指向是什么?从旧到新,还是从新到旧?索引指向什么?物理行位置,还是逻辑键?谁来清理,还有什么时候?稍后的后台进程,还是事务本身?PostgreSQL的答案是:在表中,从旧到新,物理位置,稍后的后台进程。批评者列出的每一项成本都源于这四个答案。而每个替代方案都有不同的答案,将账单送给其他人:写入者、历史的读取者、tempdb、缓存、压缩器。其中一个花费数年的工程来购买PostgreSQL的设计自第一天起就免费拥有的唯一属性。当一个事务在午餐时间保持打开时,所有替代方案都会以不同的方式失败。针对PostgreSQL的指控 如果你想了解完整的机制,PostgreSQL MVCC,Byte by Byte通过pageinspect逐步解析。简短版:在PostgreSQL中,UPDATE从不修改行。它将行的完整新副本写入堆,标记旧版本的t_xmax,并让两个版本同时留在磁盘上。可见性在读取时逐元组决定。清理是其他人的问题,具体来说是VACUUM的。以下会逐项列出四项指控。指控1:写放大 这是Uber抱怨的核心。因为表上的每个索引都指向行的物理位置(页面编号加上插槽,ctid),而UPDATE创建一个新的物理行,所以每个索引都需要一个新条目来指向新位置。即使是你没有触及的列上的索引。设置两份相同的100万行表的副本。一份只有一个主键,另一份带有四个额外的辅助索引: CREATE TABLE accounts ( id bigint PRIMARY KEY , email text NOT NULL , status text NOT NULL , balance numeric NOT NULL , created_at timestamptz NOT NULL , last_seen timestamptz ); INSERT INTO accounts SELECT g, 'user' || g || '@example.com' , 'active' , 100 , now (), now () FROM generate_series ( 1 , 1000000 ) g; CREATE INDEX ON accounts (email); CREATE INDEX ON accounts ( status ); CREATE INDEX ON accounts (balance); CREATE INDEX ON accounts (created_at); CREATE TABLE accounts_lean ( LIKE accounts); ALTER TABLE accounts_lean ADD PRIMARY KEY (id); INSERT INTO accounts_lean SELECT * FROM accounts; 现在更新100,000行,仅修改last_seen。注意last_seen在任何一个表中都不在索引中。测量生成的WAL(pg_stat_wal,先执行CHECKPOINT和pg_stat_reset_shared('wal')以获取干净的窗口): UPDATE accounts_lean SET last_seen = now () WHERE id > 100000 AND id <= 200000 ; wal_records | wal_fpi | wal_bytes | wal_pretty -------------+---------+-----------+------------ 302510 | 1419 | 38218531 | 36 MB UPDATE accounts SET last_seen = now () WHERE id > 100000 AND id <= 200000 ; wal_records | wal_fpi | wal_bytes | wal_pretty -------------+---------+-----------+------------ 709440 | 1981 | 72394573 | 69 MB 相同的逻辑变化,100,000个时间戳。精简表每行生成大约3.0个WAL记录。带索引的表产生了7.1个:堆更新,加上主键的一个新条目,加上每个四个辅助索引的一个新条目,尽管没有一个索引了我们更改的列。由于行的位置发生了变化,它们都被重写,因为它们都指向物理位置。这就是Uber测量到的放大效应,它会复合:更多的WAL意味着更多的检查点工作、更多的全页写入,以及向每个副本发送更多的字节。对一个高度索引表进行单列更新是每字节有效变化中最昂贵的事情之一。PostgreSQL的缓解措施是HOT更新(堆仅元组):如果没有索引的列更改且新版本适合同一页面,则索引被保留。

赞助内容

NordVPN Next-gen Antivirus

本站免费、广告极少。如果觉得有帮助,可以请我们喝杯咖啡 —— 任何金额都对持续运营有实际帮助。

请我喝杯咖啡