PostgreSQL 进阶:索引、MVCC、VACUUM 与 WAL

上一篇我们主要介绍了 PostgreSQL 的基础使用,包括:

text 复制代码
Database / Schema / Table
CRUD
WHERE
GROUP BY
JOIN
子查询
事务

这些内容解决的是:

PostgreSQL 怎么用?

而当数据量和并发逐渐增加后,就会遇到新的问题:

text 复制代码
为什么创建了索引,SQL 还是很慢?

为什么有时候 PostgreSQL 不走索引?

UPDATE 之后旧数据去了哪里?

为什么需要 VACUUM?

多个事务同时修改数据时如何保证一致性?

数据库宕机后为什么还能恢复?

这些问题背后就涉及 PostgreSQL 几个非常重要的机制:

text 复制代码
Index
EXPLAIN
MVCC
Lock
VACUUM
WAL

这篇文章就围绕这些内容展开。


1. 为什么需要索引?

假设有一张用户表:

sql 复制代码
CREATE TABLE users (
    id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    email TEXT NOT NULL,
    username TEXT NOT NULL,
    created_at TIMESTAMPTZ DEFAULT NOW()
);

查询某个用户:

sql 复制代码
SELECT *
FROM users
WHERE email = 'alice@example.com';

当表只有几十条数据时,全表扫描也没有什么问题。

但如果表中有几百万甚至上亿条数据,每次都扫描整张表,成本就会越来越高。

这时可以创建索引:

sql 复制代码
CREATE INDEX idx_users_email
ON users(email);

索引的作用可以简单理解为:

帮助 PostgreSQL 更快定位数据。

不过索引并不是越多越好。

因为索引本身也需要占用存储空间,并且在执行:

text 复制代码
INSERT
UPDATE
DELETE

时也需要维护。

所以索引本质上是在:

text 复制代码
查询性能
和
写入成本

之间进行权衡。


2. B-tree 索引

PostgreSQL 默认使用的索引类型是:

text 复制代码
B-tree

例如:

sql 复制代码
CREATE INDEX idx_users_email
ON users(email);

B-tree 比较适合:

text 复制代码
=
>
<
>=
<=
BETWEEN
ORDER BY

例如:

sql 复制代码
SELECT *
FROM users
WHERE email = 'alice@example.com';

或者:

sql 复制代码
SELECT *
FROM users
WHERE created_at >= '2026-01-01'
ORDER BY created_at;

大部分普通业务查询中,B-tree 都是最常见的索引。


3. GIN、GiST 和 BRIN

PostgreSQL 除了 B-tree,还支持多种索引类型。

比较常见的有:

text 复制代码
GIN
GiST
BRIN

GIN

GIN 经常用于:

text 复制代码
JSONB
Array
全文搜索

例如:

sql 复制代码
CREATE INDEX idx_events_data
ON events
USING GIN(data);

如果业务中经常查询 JSONB 内部的数据,GIN 非常常见。

GiST

GiST 经常用于:

text 复制代码
地理空间数据
Range
几何类型
相似度查询

例如 PostGIS 就经常使用 GiST。

BRIN

BRIN 比较适合:

text 复制代码
超大表
+
数据具有明显顺序特征

例如日志表中的:

text 复制代码
created_at

可以创建:

sql 复制代码
CREATE INDEX idx_logs_created_at
ON logs
USING BRIN(created_at);

BRIN 的索引体积通常比 B-tree 小很多,因此很适合大型时间序列表。


4. 联合索引

索引可以包含多个字段:

sql 复制代码
CREATE INDEX idx_orders_user_status
ON orders(user_id, status);

这种索引就叫联合索引。

例如:

sql 复制代码
SELECT *
FROM orders
WHERE user_id = 100
  AND status = 'paid';

可能很好地利用这个索引。

联合索引需要特别注意字段顺序。

例如:

text 复制代码
(user_id, status)

和:

text 复制代码
(status, user_id)

虽然包含相同字段,但效果可能完全不同。

设计联合索引时需要结合:

text 复制代码
查询条件
数据分布
字段选择性
排序需求

一起考虑。


5. Partial Index 和 Expression Index

PostgreSQL 还有两个非常实用的索引设计。

Partial Index

假设系统大部分查询只查未删除数据:

sql 复制代码
WHERE deleted_at IS NULL

可以创建:

sql 复制代码
CREATE INDEX idx_active_users_email
ON users(email)
WHERE deleted_at IS NULL;

也就是只给部分数据建立索引。

这种索引通常可以减少索引体积。

Expression Index

如果系统经常这样查询:

sql 复制代码
SELECT *
FROM users
WHERE LOWER(email) = 'alice@example.com';

可以直接针对表达式建索引:

sql 复制代码
CREATE INDEX idx_users_lower_email
ON users(LOWER(email));

说明 PostgreSQL 的索引设计并不只是:

text 复制代码
给某个字段加一个索引

而是应该根据真实查询方式设计。


6. 为什么有索引还会 Seq Scan?

假设:

sql 复制代码
SELECT *
FROM users
WHERE is_active = TRUE;

并且:

text 复制代码
95% 用户都是 active

即使给 is_active 创建索引,PostgreSQL 也可能选择:

text 复制代码
Seq Scan

也就是全表扫描。

原因是 PostgreSQL 会进行成本估算。

如果最终需要读取表中绝大多数数据:

text 复制代码
通过索引找到大量记录
+
再次访问表

可能还不如:

text 复制代码
直接扫描整个表

所以:

text 复制代码
Seq Scan

并不代表 SQL 一定有问题。

PostgreSQL 会根据数据量、统计信息和预估成本选择执行方式。


7. EXPLAIN 和 EXPLAIN ANALYZE

如果想知道 PostgreSQL 是怎么执行 SQL 的,可以使用:

sql 复制代码
EXPLAIN
SELECT *
FROM users
WHERE email = 'alice@example.com';

常见执行方式包括:

text 复制代码
Seq Scan
Index Scan
Bitmap Index Scan
Bitmap Heap Scan

如果使用:

sql 复制代码
EXPLAIN ANALYZE
SELECT *
FROM users
WHERE email = 'alice@example.com';

PostgreSQL 会真正执行 SQL,并返回实际运行数据。

常见信息包括:

text 复制代码
cost
rows
actual time
actual rows
loops

分析慢 SQL 时,非常重要的一点是比较:

text 复制代码
estimated rows

和:

text 复制代码
actual rows

如果差距特别大,可能说明 PostgreSQL 对数据分布的估计不够准确。

需要注意:

text 复制代码
EXPLAIN ANALYZE

是真的会执行 SQL。

所以对:

text 复制代码
UPDATE
DELETE
INSERT

使用时一定要谨慎。


8. 什么是 MVCC?

MVCC 全称:

text 复制代码
Multi-Version Concurrency Control

即:

text 复制代码
多版本并发控制

它主要解决的问题是:

多个事务同时读取和修改数据时,如何减少互相阻塞,同时保证数据一致性?

PostgreSQL 会维护不同的数据版本。

不同事务根据自己的:

text 复制代码
Snapshot

判断哪些数据对自己可见。

可以简单理解成:

text 复制代码
事务 A
看到数据版本 A

事务 B
看到数据版本 B

因此 PostgreSQL 的普通读操作和写操作之间通常不需要互相阻塞。

这也是 PostgreSQL 并发能力的重要基础。


9. UPDATE 之后发生了什么?

执行:

sql 复制代码
UPDATE users
SET username = 'Bob'
WHERE id = 1;

很多人会认为数据库只是直接把:

text 复制代码
Alice

改成:

text 复制代码
Bob

但从 MVCC 的角度理解,PostgreSQL 通常会生成新的数据版本。

可以简单理解成:

text 复制代码
旧版本
id = 1
username = Alice

↓

新版本
id = 1
username = Bob

旧版本不能马上删除。

因为可能还有其他事务需要看到这个旧版本。

等这些旧版本已经不会被任何事务访问后,它们就会逐渐变成:

text 复制代码
Dead Tuple

这也就引出了 VACUUM。


10. VACUUM 是什么?

由于 PostgreSQL 的 MVCC 会产生旧版本数据,如果这些数据一直不处理,就会占用越来越多空间。

因此 PostgreSQL 需要:

sql 复制代码
VACUUM users;

VACUUM 的一个重要作用就是:

清理已经不再需要的旧数据版本,并让这些空间可以被 PostgreSQL 再次使用。

不过需要注意:

普通:

sql 复制代码
VACUUM;

通常并不会让操作系统看到的表文件立即变小。

它更像是:

text 复制代码
这部分空间已经没用了
↓
以后 PostgreSQL 可以重新使用

如果更新、删除非常频繁,而 VACUUM 又没有及时处理,就可能产生:

text 复制代码
Table Bloat

也就是表膨胀。


11. VACUUM FULL

如果使用:

sql 复制代码
VACUUM FULL users;

PostgreSQL 会重新整理并重写整张表。

这样有机会真正缩小表文件。

但代价也更高:

text 复制代码
执行时间更长
需要更强的锁
可能影响业务

所以:

text 复制代码
VACUUM FULL

通常不是日常维护命令。

普通场景主要依赖:

text 复制代码
VACUUM
+
Autovacuum

12. Autovacuum

如果所有表都需要人工执行:

sql 复制代码
VACUUM;

显然不现实。

所以 PostgreSQL 提供:

text 复制代码
Autovacuum

自动维护机制。

它会根据表的数据变化情况自动进行:

text 复制代码
VACUUM
ANALYZE

其中:

text 复制代码
VACUUM

主要处理旧数据版本。

而:

text 复制代码
ANALYZE

主要收集数据统计信息。

这些统计信息会帮助 PostgreSQL 判断:

text 复制代码
走索引还是全表扫描?

选择哪种 JOIN?

预计会返回多少数据?

所以 Autovacuum 不只是清理空间,它也会间接影响 SQL 的执行计划和性能。


13. MVCC 不代表没有锁

有 MVCC 并不意味着 PostgreSQL 不需要锁。

例如:

sql 复制代码
SELECT *
FROM accounts
WHERE id = 1
FOR UPDATE;

FOR UPDATE 会锁住对应的数据行。

常见场景:

text 复制代码
读取账户余额
↓
修改账户余额

例如:

sql 复制代码
BEGIN;

SELECT balance
FROM accounts
WHERE id = 1
FOR UPDATE;

UPDATE accounts
SET balance = balance - 100
WHERE id = 1;

COMMIT;

所以可以简单理解为:

text 复制代码
MVCC
主要解决读写并发

Lock
主要协调真正的数据修改冲突

两者是配合工作的。


14. 什么是 WAL?

上一篇介绍事务时提到 ACID。

其中:

text 复制代码
Durability

也就是持久性,和 WAL 有非常重要的关系。

WAL 全称:

text 复制代码
Write-Ahead Logging

即预写式日志。

核心思想是:

数据页真正写入磁盘之前,先记录对应的 WAL。

可以简单理解:

text 复制代码
INSERT / UPDATE / DELETE
        ↓
       WAL
        ↓
    数据文件

这样做的好处是:

如果数据库突然:

text 复制代码
断电
进程崩溃
服务器异常

PostgreSQL 可以利用 WAL 进行恢复。

这也是 PostgreSQL 保证数据可靠性的核心机制之一。


15. WAL 和事务提交

例如一个事务:

sql 复制代码
BEGIN;

UPDATE accounts
SET balance = balance - 100
WHERE id = 1;

COMMIT;

对应的数据修改会产生 WAL。

事务提交时,PostgreSQL 会确保关键 WAL 已经可靠持久化。

因此:

text 复制代码
COMMIT

并不等于:

所有修改过的数据页已经全部写回表文件。

即使某些数据页还没有真正写回磁盘,只要 WAL 已经安全持久化,发生崩溃后 PostgreSQL 依然可以利用 WAL 恢复数据。

这使 PostgreSQL 可以兼顾:

text 复制代码
性能
+
可靠性

16. Checkpoint

既然 PostgreSQL 可以利用 WAL 恢复数据,那么自然还有一个问题:

数据库恢复时,总不能从很久以前的 WAL 开始重放吧?

所以 PostgreSQL 还有:

text 复制代码
Checkpoint

可以把它简单理解为:

一个新的恢复基准点。

Checkpoint 会把一部分脏数据页写回磁盘,并推进数据库的持久化状态。

这样发生崩溃后,就不需要从无限久以前开始恢复。

Checkpoint 会涉及:

text 复制代码
IO
shared_buffers
max_wal_size
checkpoint_timeout

这些内容更适合放到后面的性能优化篇深入讨论。


17. WAL 和 Replication

WAL 除了用于崩溃恢复,也是 PostgreSQL 复制机制的重要基础。

最常见的结构:

text 复制代码
Primary
   │
   │ WAL
   ↓
Standby

Primary 产生 WAL。

Standby 接收并重放 WAL。

这样就可以实现:

text 复制代码
高可用
灾备
读扩展

不过 Primary 和 Standby 之间可能存在:

text 复制代码
Replication Lag

也就是复制延迟。

因此刚刚写入 Primary 的数据,不一定马上可以在 Standby 上查询到。


18. 把这些机制串起来

现在可以把几个核心概念串在一起。

执行:

sql 复制代码
UPDATE users
SET username = 'Bob'
WHERE id = 1;

背后可以粗略理解为:

text 复制代码
UPDATE
  │
  ├── MVCC
  │     ↓
  │   产生新的数据版本
  │
  ├── Lock
  │     ↓
  │   协调并发修改
  │
  └── WAL
        ↓
      记录修改

旧版本随着时间推移:

text 复制代码
旧版本
  ↓
Dead Tuple
  ↓
VACUUM
  ↓
空间重新利用

而查询 SQL 时:

text 复制代码
SQL
 ↓
Planner
 ↓
统计信息
 ↓
成本估算
 ↓
Seq Scan / Index Scan
 ↓
执行查询

这样一来:

text 复制代码
Index
EXPLAIN
MVCC
VACUUM
WAL

其实就是 PostgreSQL 不同运行机制之间的一条完整链路。


总结

如果说 PostgreSQL 入门阶段解决的是:

PostgreSQL 怎么用?

那么进阶阶段开始解决的是:

PostgreSQL 为什么这样工作?

这一篇最重要的内容可以总结成:

text 复制代码
Index
提升查询效率

EXPLAIN
查看 SQL 执行方式

MVCC
通过多版本处理并发访问

Lock
解决数据修改冲突

VACUUM
清理 MVCC 产生的旧版本

Autovacuum
自动维护表和统计信息

WAL
保证恢复能力和事务持久性

Replication
利用 WAL 实现数据复制
相关推荐
zhangjw341 小时前
1-1 PostgreSQL 多平台环境部署:Linux|Docker|Windows|MacOS 完整实操
linux·docker·postgresql
天空鸟_时光不老1 小时前
10-Agent安全隐患与SQL执行层加固
java·数据库·人工智能·sql·spring·maven·mybatis
晚风叙码2 小时前
MySQL 表的约束详解:从空属性到外键
android·数据库·mysql·adb
IT古董10 小时前
《FDE前沿部署工程师实战教程》33 - Enterprise AI Observability:从Agent Trace到全链路智能运维
数据库·人工智能
for_ever_love__10 小时前
MySQL 主从复制讲透:binlog 原理、搭建步骤与读写分离
java·数据库·mysql·binlog·读写分离·原理·主从复制
Nturmoils14 小时前
GROUP BY 先别想当然,查汇总前把规则跑清楚
数据库
最笨的羊羊15 小时前
Oceanbase数据库系列之:向量数据库核心知识
数据库·oceanbase
fish_xk15 小时前
mysql中的表的约束
数据库·mysql
谢亮_vipxieliang15 小时前
Go Channel 高级模式——从底层原理到扇出扇入实战
java·数据库·golang