目录

一、为什么 PostgreSQL 和 MySQL 值得放在一起比较

二、核心差异

三、PostgreSQL是什么

四、架构模型

1. MySQL:单进程多线程

2. PostgreSQL:多进程模型

五、MVCC

1. 什么是 MVCC?

2. MySQL vs PostgreSQL

六、VACUUM

七、事务与隔离级别

八、锁机制

九、回滚与恢复

十、WAL 与 Checkpoint

十一、索引体系

十二、系统性

十三、pgvector

十四、总结


一、为什么 PostgreSQL 和 MySQL 值得放在一起比较

在关系型数据库领域,MySQL 和 PostgreSQL 经常被同时提起。
原因并不只是它们都很流行,而是因为它们都在解决同一类问题:

  • 如何可靠地存储业务数据
  • 如何支持事务
  • 如何处理并发访问
  • 如何加速查询
  • 如何在故障后恢复

但它们的解决方式并不完全一样。

很多时候,真正值得比较的不是:

  • 谁的语法更好记
  • 谁的工具更多

而是它们如何处理数据版本、事务一致性、并发冲突、日志恢复以及索引组织。

这也是本文的重点。

并且在当下,PostgreSQL往往在关系型数据库领域中成为更优的选择,其原因也会在本文中进行展开


二、核心差异

这里用这个表格大体总览一下,后续会进行详细展开。


三、PostgreSQL是什么

PostgreSQL 是一个开源的对象-关系型数据库管理系统。

很多人会把它简单理解成另一个支持事务的关系数据库,就像MySQL一样。

这些说法都不算错,但不够准确。

PostgreSQL 是一个以事务一致性、多版本并发控制、复杂查询能力和扩展性见长的数据库系统。

它不仅关注把数据存进去,还关注:

  • 如何严谨地表达事务
  • 如何优雅地处理并发
  • 如何支持复杂类型和复杂查询
  • 如何让数据库能力本身具备平台化特征

MySQL 的成功来自大量互联网业务实践,它在经典 OLTP 场景里非常稳。
而 PostgreSQL 的优势在于,很多机制不是后来补上的能力,而是一开始就在围绕事务、快照、版本和扩展性做统一设计。

这种差异会在后面的 MVCC、锁、事务与索引章节里越来越明显。


四、架构模型

1. MySQL:单进程多线程

MySQL 通常以一个 mysqld 进程运行,内部通过多个线程完成工作,例如:

  • 处理客户端连接
  • 执行 SQL
  • 管理 buffer pool
  • redo 日志刷盘
  • 后台清理
  • 主从复制相关任务

这种方式的特点是:

  • 线程模型比较经典
  • 对大量连接的适配相对自然
  • 线程切换成本通常低于进程切换

2. PostgreSQL:多进程模型

PostgreSQL是:

  • 一个主控进程 postmaster
  • 每个连接通常对应一个 backend 进程
  • 另外有多个后台进程负责不同职责

优势

  • 连接进程之间隔离更明显
  • 单个连接崩溃影响更局部
  • 后台职责划分更清晰

代价

  • 连接成本更高
  • 更依赖连接池
  • 太多空闲连接会造成资源浪费

五、MVCC

1. 什么是 MVCC?

MVCC,全称Multi-Version Concurrency Control,即多版本并发控制。

它要解决的问题很简单:

在有并发事务时,怎样尽量做到读不阻塞写,写不阻塞读,同时保证隔离性。

如果数据库只允许当前值存在,那么:

  • 一个事务正在写
  • 另一个事务读同一行
  • 就很容易发生阻塞

MVCC 的核心思路是:

  • 不只保留一个版本
  • 而是允许同一条记录存在多个版本
  • 不同事务根据自己的快照,看到不同版本

这就是多版本的本质。

2. MySQL vs PostgreSQL

MySQL(InnoDB)的做法

  • 数据页中保存当前版本
  • 历史版本保存在 undo log
  • 查询旧版本时,需要通过 undo 链回溯

PostgreSQL 的做法

  • 旧版本和新版本都直接保留在表里
  • UPDATE 不做原地修改,而是插入一个新版本
  • 事务根据 snapshot 决定自己看到哪个版本

可以用一句话概括就是:MySQL 是当前值 + undo 回溯过去,PostgreSQL 是过去和现在都保留,由快照决定看哪个。

在 PostgreSQL 里,一条记录不是单纯的一行值,更准确地说,是一个个tuple version。

每个版本都会带一些隐藏系统字段,最重要的是:

  • xmin:创建该版本的事务 ID
  • xmax:使该版本失效的事务 ID

这两个字段,几乎就是 PostgreSQL 可见性判断的核心。

看一条很普通的 SQL:UPDATE users SET name = 'Bob' WHERE id = 1;

很多人下意识会认为数据库做的是:

  • 找到记录
  • 把 name 从 Alice 改成 Bob

但 PostgreSQL 的真实思路是先旧版本被标记为“已被当前事务更新”,再插入一个新版本,后续事务根据快照选择看到旧版本还是新版本。

这意味着 PostgreSQL 的 UPDATE 本质不是“原地改写”,而是“版本追加”。

为什么同一行数据,不同事务看到的结果可能不同?

在 PostgreSQL 里,判断一条记录是否“存在”,不是简单看磁盘上有没有那条数据。
它更像是在问:这个版本对当前事务是否可见?

判断依据通常包括:

  • 创建该版本的事务是否已提交
  • 删除或更新该版本的事务是否已提交
  • 当前事务的 snapshot 中,这些事务是否可见

所以 PostgreSQL 对数据的理解不是绝对当前值,而是:

  • 版本 + 事务状态 + 快照

什么是Snapshot?

PostgreSQL 会为事务或语句分配 snapshot。
你可以把它理解成一张当前数据库世界的快照。

这张照片里包含:

  • 哪些事务已经提交
  • 哪些事务仍然活跃
  • 哪些事务对当前事务来说还不可见

于是同一张表,不同事务可能读到不同版本,这不是异常,而是 PostgreSQL 设计的一部分。


那PostgreSQL的Snapshot不就和MySQL的Read View一样吗?

概念确实非常相似,但是实现链路不一样。

MySQL

  1. 先看到当前版本
  2. 如果不可见,沿 undo log 往回找历史版本
  3. 再判断哪个版本可见

PostgreSQL

  1. 直接扫描到 tuple 版本
  2. 根据 xmin/xmax + snapshot 判断该版本是否可见

所以从实现观感上看:

  • MySQL:更像当前数据 + 历史回放
  • PostgreSQL:更像版本天然共存 + 快照选择

这也是很多人觉得 PostgreSQL 的 MVCC 更原生的原因。


六、VACUUM

UPDATE 和 DELETE 都不会立刻把旧版本物理删除。
于是就会留下那些:

  • 对当前和未来事务都已经没有意义
  • 但物理上还占着空间的版本。

这些无效版本就叫dead tuples。

既然 PostgreSQL 会保留旧版本,那一个自然的问题就是:

旧版本什么时候清理?

这就是 VACUUM 存在的原因。

VACUUM 的主要职责包括:

  1. 清理 dead tuples
  2. 回收空间供后续复用
  3. 更新可见性信息
  4. 防止事务 ID 回卷问题

什么是事务ID回卷问题?

就是数据库的事务编号不是无限的,编号用久了会绕回去,如果旧事务信息没有及时清理,就可能导致可见性判断错误,严重时数据库会停止写入以自保。

VACUUM的处理方式就是把那些已经古老到不可能再变化的数据,标记成“足够老、永远可见”,这样即使事务 ID 回绕,也不会误判。

注意一个常见误区:普通 VACUUM 并不一定马上缩小表文件大小,它更多是在回收内部可复用空间。

为什么 PostgreSQL 特别怕长事务?

长事务会一直持有旧 snapshot。
只要这个 snapshot 还在,某些旧版本就可能仍然“理论上可见”,因此 VACUUM 不敢清理。

结果就是:

  • dead tuples 堆积
  • 表膨胀
  • 索引膨胀
  • autovacuum 压力上升
  • 查询性能变差

因此长事务是 PostgreSQL 的典型性能风险之一。


什么是autovacuum?

线上几乎不会靠人工频繁执行 VACUUM。
PostgreSQL 依赖的是autovacuum,它会自动做vacuum。

如果说 MySQL 线上很多时候重点关注的是慢 SQL、锁等待、索引命中

那么 PostgreSQL 通常还必须把这几个问题加入重点观察:

  • autovacuum 是否跟得上
  • 是否存在长事务
  • 是否发生 heap / index bloat

七、事务与隔离级别

MySQL和PostgreSQL都支持经典的 ACID 特性:

  • Atomicity:原子性
  • Consistency:一致性
  • Isolation:隔离性
  • Durability:持久性

但 PostgreSQL 经常给人的感觉是它不只是支持事务,而是很多机制从一开始就是围绕事务一致性设计出来的。

需要注意,MySQL 默认级别是RR,而PostgreSQL是RC。

RC的并发粒度比RR要小,行为对很多业务开发来说更加直观,锁开销更小,更适合当下企业业务场景。


除此之外,PostgreSQL中的RU级别其实完全等同于RC级别。

按SQL标准,RU级别是允许出现脏读的。

而PostgreSQL的MVCC设计,只允许select读取的是一个已经提交并且可见的版本。

PostgreSQL宁可取消一个隔离级别,也不愿意去放开不一致读取。

这也非常能体现出PostgreSQL的设计理念。

针对Serializable,二者的实现差异也很大。

MySQL

依赖、gap lock、next-key lock、范围锁

本质上是更偏“提前锁住潜在风险”。

PostgreSQL

PostgreSQL 的 Serializable 采用的是SSI(Serializable Snapshot Isolation)

它的工作方式大致是:

  1. 先按快照隔离执行事务
  2. 系统跟踪事务之间的读写依赖
  3. 如果发现可能形成不可串行化的冲突,就中止其中一个事务

这是一种非常有代表性的现代数据库设计思路:

尽量保持并发,而不是一开始就把所有可能性锁死。

也正因为如此,很多人会觉得 PostgreSQL 的事务控制更“高级”,因为它不是简单依靠“加更多锁”来实现更高隔离。


八、锁机制

有了 MVCC,并不意味着锁失效了。
MVCC 解决的是版本可见性,而不是所有并发问题。

例如:

  • 两个事务同时更新同一行
  • DDL 和 DML 冲突
  • 外键约束检查
  • 唯一约束竞争
  • 某些业务需要显式互斥

这些仍然需要锁。

那么PostgreSQL有哪些锁?

常见可以分为:

  1. 表级锁
  2. 行级锁
  3. 页级锁
  4. 咨询锁
  5. 谓词锁

什么是咨询锁?

咨询锁是数据库提供给应用程序使用的业务级自定义锁,它不直接锁表或锁行,而是让开发者基于某个自定义 key 做互斥控制,比如防止同一任务重复执行、同一用户并发处理等,但前提是所有代码都要主动遵守这套规则。

也就是说:数据库只是提供这把锁,至于大家用不用、遵不遵守,要靠应用自己约定。


什么是谓词锁?

谓词锁则是 PostgreSQL 在 SERIALIZABLE 隔离级别下自动使用的一种内部并发控制机制,它不是给业务手工加的,也不是像 MySQL 的 gap lock 那样直接阻止别人插入,而是用来记录事务读过的条件范围,当其他事务修改了这个范围时,系统再基于 SSI 检测是否存在不可串行化冲突,必要时回滚其中一个事务。

简而言之,就是这个锁负责记录读过的范围,如果有其他事务对这个范围的tuple进行了修改或者增删了,系统就会立即检测到两个事务发生了冲突,有可能造成不可重复读或者幻读等问题,就会回滚其中一个事务。

这里我们就重点讲解一下行级锁:

MySQL 的行锁本质上更依赖索引,锁的往往不只是某条记录,还可能连同索引间隙一起锁住,也就是常说的 record lock、gap lock 和 next-key lock;而 PostgreSQL 的行锁更偏向锁住实际命中的那几行记录本身,通常不会顺带把某个索引范围内未来可能插入的数据也锁住。

这就导致在 MySQL 中,像 SELECT ... FOR UPDATE 或范围更新语句,在RR级别下很可能不仅阻塞别人修改已有行,还会阻塞别人往这个范围里插入新行,以防止幻读;而 PostgreSQL 一般只阻塞别人修改或删除已锁定的行,不会天然阻止新行插入,因此并发更高。

MySQL的间隙锁和临键锁是为了防止幻读的,因此牺牲了部分并发换取数据安全。而PostgreSQL的锁只是单纯锁住了tuple,那不会出现幻读问题吗?

快照读情况下不必多说,只需要遵循同一份快照自然就避免了幻读问题。

关键是当前读的情况,PostgreSQL里其实并没有MySQL中当前读的说法,我们这里实际上指的是select for update/update/delete这类情况,这类情况不能使用旧快照,但是PG也不会让他们破坏快照,而是先按照事务找到候选行,如果这行被别的事务更改过则先等待,当那个事务提交之后,若这行版本与事务开始时不一致,则会报错。

所以PG宁愿是走报错方式,也不愿意破坏快照。

因此RR级别下的PG也是完美避开了幻读问题。


有些同学会觉得,PostgreSQL 在 RR 级别下遇到并发更新时需要等待、重新检查,甚至直接报错回滚重试,这不是降低并发吗?

确实,从单个发生冲突的事务来看,并发度是下降了,因为它可能被阻塞或失败重试。但 PostgreSQL 的优势在于:它不会为了防止幻读或保证一致性,像MySQL那样大量使用范围锁、间隙锁去阻塞其他事务,而是依靠 MVCC 快照读,让大多数普通 SELECT 都可以在不加锁的情况下直接读取一致性视图,不会被写事务阻塞,也不会去阻塞写事务。这样虽然少数“读后又要修改”的事务会在冲突时付出代价,但从整个系统看,更多请求都能并行执行,快照一致性也没有被破坏,因此总体并发性通常更高。

在死锁方面,PG和MySQL的处理方式大致相同:

  • 自动检测死锁
  • 中止其中一个事务
  • 返回错误给应用
  • 由应用决定是否重试

PG不像MySQL拥有间隙锁和临键锁,所以死锁的概率会小一些。通常是因为访问顺序交错导致的哲学家问题。


九、回滚与恢复

在MySQL InnoDB 中,undo log 既承担:

  • 一致性读构造旧版本
  • 事务失败时撤销修改

也就是说,undo 是一个双重角色:

  • 既服务 MVCC
  • 又服务回滚

而PostgreSQL的 UPDATE 本来就是生成新版本。
因此当事务回滚时:

  • 这些新版本不会成为有效数据
  • 对其他事务来说本来就不可见
  • 后续由 VACUUM 清理即可

所以 PostgreSQL 的回滚更像是:

让当前事务生成的新版本永远失效,而不是把值原地改回旧状态。

这是一种更符合多版本思维的实现方式。


十、WAL 与 Checkpoint

事务最终一定要落到一个问题上:如果数据库突然宕机,已提交的数据如何保证不丢?

    PG的策略则是WAL,核心原则就是在数据页落盘之前必须先让日志落盘。

    所以对于PG来说:事务提交成功的判据,不是数据页已经刷回表文件,而是 WAL 已经持久化。

    但是如果数据库每次崩溃恢复都要从很久以前的 WAL 开始重放,恢复时间会非常长。
    因此 PostgreSQL 需要 checkpoint。

    Checkpoint 的作用是:

    • 把部分脏页刷回磁盘
    • 记录恢复起点
    • 缩短后续崩溃恢复时间

    但 checkpoint 也有平衡问题:

    • 太频繁:刷盘压力大、I/O 抖动明显
    • 太少:恢复时间变长、WAL 堆积更多

    这也是 PostgreSQL 调优里经常要关注的一类参数。

    PostgreSQL 在备份恢复上的一项非常强的能力是:PITR(Point-In-Time Recovery)

    基本思路是:

    1. 做 base backup
    2. 持续归档 WAL
    3. 恢复时先恢复基线备份
    4. 再把 WAL 回放到某个目标时间点

    这意味着:

    • 误删数据后可以恢复到误删前一刻
    • 灾难恢复更可控
    • 一致性恢复链路很清晰

    MySQL 也能通过全量备份 + binlog 实现类似能力,但 PostgreSQL 的 WAL 体系在这一点上更统一、原生。


    十一、索引体系

    MySQL InnoDB 的核心索引结构是 B+Tree。
    它非常适合:

    • 主键查询
    • 范围查询
    • 排序
    • 常见 OLTP 业务

    对于传统互联网场景来说,这套体系已经非常成熟,也非常高效。

    所以如果业务模型比较经典,MySQL 的索引并不弱。

    而PG的索引优势则体现在多样性。

    PG支持的常见索引类型包括:

    • 查询 JSON 字段里的某个属性
    • 模糊检索文本
    • 搜索数组元素
    • 地理空间范围查询
    • 时间范围日志检索
    • 区间重叠判断
    • 文档或向量相关查询

    在这些场景下,B+Tree 往往不是唯一答案,甚至不是最佳答案。

    这时 PostgreSQL 的索引体系就会显得更自然。

    最常用的如下:

    1. B-tree

    适合等值、范围、排序,是最通用的索引。

    2. GIN

    适合:

    • JSONB
    • 数组
    • 全文检索

    3. GiST

    适合:

    • 地理空间
    • 范围类型
    • 相似搜索

    4. BRIN

    适合超大表,尤其是按时间或顺序追加的数据。
    它不保存每行细节,而保存块范围摘要,因此:

    • 索引体积很小
    • 创建很快
    • 非常适合日志表、时间序列表

    除了多样性以外,PG还有两个高价值特性:表达式索引和部分索引:

    表达式索引

    -- 表示当使用小写参数时索引生效
    CREATE INDEX idx_lower_email ON users(lower(email));

    部分索引

    -- 只有当deleted字段为false的情况下才会建索引
    CREATE INDEX idx_active_user ON users(id) WHERE deleted = false;

    这两个索引在实际业务场景都非常使用,可以实现加速查询的同时节省空间。

    最后,HOT Update也是PG不可忽视的特性之一。

    Heap-Only Tuple Update,意思是只在堆表里完成的更新。

    如果一次 UPDATE 没有修改任何索引列,那么 PostgreSQL 尽量只在表(heap)里生成新版本,而不去更新索引。

    当满足条件时:

    • 旧 tuple 保留
    • 新 tuple 写到同一个 heap page
    • 旧 tuple 指向新 tuple,形成一条 HOT 链
    • 索引仍然指向这条链的起点,不需要新增索引项

    查询时,PG 通过这个链找到当前可见的版本。

    因此,HOT Update的好处有很多:

    1. 减少了索引维护的开销,不需要变动索引页
    2. 降低了WAL量,不需要变更索引,日志量自然也变少了
    3. 减轻了索引膨胀,不会为每次更新都产生新的索引项
    4. 提高高频更新场景的性能,无需变动索引页,更新效率大大提高

    十二、系统性

    这张图说明 PostgreSQL 的核心机制并不是一堆彼此独立的模块,而是围绕几个底层基础不断展开的:

    • tuple version
    • snapshot
    • WAL
    • VACUUM
    • 索引可见性

    因此你会发现:

    • MVCC 会影响事务可见性
    • MVCC 会影响 VACUUM
    • VACUUM 又会影响索引扫描表现
    • WAL 影响提交与恢复
    • Serializable 又建立在快照与依赖分析之上

    这就是 PostgreSQL 常被评价为“设计统一”的原因。


    十三、pgvector

    在近几年的 AI 应用中,向量检索已经成为一个很常见的需求。

    PostgreSQL 本身不是专门的向量数据库,但它可以通过扩展 pgvector 来支持这类场景。

    它为 PG 增加了:

    • 向量类型 vector(n)
    • 向量距离计算
    • 相似度查询
    • 向量索引能力

    安装扩展后,可以直接在表中定义向量字段,例如:

    CREATE EXTENSION vector;
    
    CREATE TABLE documents (
        id bigserial PRIMARY KEY,
        content text,
        embedding vector(768)
    );

    这里的 embedding vector(768) 表示该字段存储一个 768 维向量。

    pgvector 的意义不只是PostgreSQL 也能查向量,而是PostgreSQL 可以把关系数据、JSON 数据、事务能力、SQL 过滤条件和向量检索放到同一个系统里处理。

    比如你可以一边做向量相似度搜索,一边加上业务过滤条件:

    SELECT id, content
    FROM documents
    WHERE tenant_id = 1001
    ORDER BY embedding <=> '[...]'
    LIMIT 10;

    十四、总结

    到这里,其实已经不需要再去单独讨论“为什么 PostgreSQL 越来越受关注”了。
    因为从上面的机制对比中,结论已经很自然:

    • 当你只需要一个成熟、稳定、经典的事务型数据库时,MySQL 依然非常优秀
    • 但当系统开始要求更强的事务语义、更统一的多版本模型、更灵活的索引、更复杂的查询和更强的扩展能力时,PostgreSQL 往往会更自然地适配这些需求

    PostgreSQL 的优势,不在某个单点能力,而在于它把 MVCC、事务、锁、恢复和索引做成了一套统一系统。


    ~码文不易,留个赞再走呗~

    Logo

    AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

    更多推荐