上一篇我们主要介绍了 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 实现数据复制