文章

PostgreSQL 架构与核心特性

PostgreSQL 架构与核心特性

概述

PostgreSQL 是功能最丰富的关系型开源数据库,以 ACID 强一致、扩展性强(FDW/PG/PL)、MVCC 实现优雅著称,在 OLTP + OLAP 混合场景和地理信息系统中优势明显。

MySQL vs PostgreSQL 关键差异

维度MySQLPostgreSQL
MVCC 实现Undo Log 版本链元组多版本(旧版本留在堆中)
VACUUM不需要✅ 必须做,回收死元组
索引类型B+Tree, 全文, 空间(R-Tree)B-Tree, Hash, GiST, GIN, SP-GiST, BRIN
并发查询单进程多线程多进程(每个连接一个进程)
DDL 事务❌(隐式提交,8.0 支持原子 DDL)✅ 原生支持
JSONJSON 类型 + 函数JSONB(二进制)+ GIN 索引,查询极快
扩展性插件有限FDW(外部数据包装器)、自定义类型/操作符/索引
复制主从复制流复制 + 逻辑复制 + 级联复制
存储引擎可插拔(InnoDB/MyISAM 等)统一引擎(不可插拔)
连接池需求可忽略强烈需要(PgBouncer/Pgpool-II)

并发模型:多进程架构

         Client

    ┌──────────────┐
    │  Postmaster   │ (主进程,fork + 管理)
    └──┬───┬───┬───┘
       ↓   ↓   ↓
   Backend  Backend  Backend   (每个连接一个进程)
       ↓       ↓       ↓
    ┌──────────────────────────┐
    │    Shared Buffer Pool    │ (共享内存)
    ├──────────────────────────┤
    │    WAL Buffer            │
    ├──────────────────────────┤
    │    Lock Table            │
    └──────────────────────────┘

    ┌──────────────────────────┐
    │       数据文件             │
    └──────────────────────────┘

连接池是必须的: 每个 PG 后端进程约 10MB。1000 个连接 = 10GB 内存。

# 推荐 PgBouncer 生产配置
pool_mode = transaction       # 事务结束后归还连接
default_pool_size = 80        # 每个数据库/用户的连接池大小
max_client_conn = 2000        # 最大客户端连接

MVCC 实现:元组多版本(关键差异)

PG 的 MVCC 特点

MySQL MVCC:
  Undo Log 保存旧版本 → 通过回滚指针串联 → Purge 清理

PG MVCC:
  旧元组直接保留在堆中(不移动)→ 通过可见性规则判断 → VACUUM 回收

核心概念:每个元组有 4 个系统列

列名含义
xmin插入此元组的事务 ID
xmax删除/更新此元组的事务 ID(0 表示未删除)
cmin插入时的事务内命令 ID
cmax删除时的事务内命令 ID

UPDATE 在 PG 中的实际行为

-- UPDATE users SET name='new' WHERE id=1;
-- 不是原地修改,而是:
-- 1. 新元组:INSERT 新版数据(xmin=当前 txid)
-- 2. 旧元组:标记 xmax=当前 txid(逻辑删除)
-- 3. 更新索引指向新元组
-- → 旧元组等待 VACUUM 回收

从这可以看出 PG 的 MVCC 设计哲学:不覆盖数据,版本自然堆叠。好处是实现简单、回滚几乎零成本。代价是必须靠 VACUUM 清理,且更新频繁时表膨胀严重。

VACUUM —— PG 运维核心

为什么 VACUUM 如此重要

Dead Tuples(死元组)越来越多
    → 表膨胀(磁盘/内存浪费)
    → 扫描效率下降(需要检查更多元组)
    → 事务 ID 回卷风险(xid 用尽 → 数据库 shutdown)

AUTO VACUUM 自动清理

-- 查看 AUTO VACUUM 状态
SHOW autovacuum;                  -- 强烈建议 ON(默认)
SHOW autovacuum_vacuum_scale_factor;   -- 0.2(死元组 20% 触发)
SHOW autovacuum_vacuum_threshold;      -- 50 行

-- 大表调优(降低阈值让 VACUUM 更频繁)
ALTER TABLE large_table SET (
    autovacuum_vacuum_scale_factor = 0.05,    -- 5% 触发
    autovacuum_vacuum_threshold = 10000
);

-- 查看 VACUUM 进度
SELECT * FROM pg_stat_progress_vacuum;

VACUUM vs VACUUM FULL

操作回收空间锁级别是否会阻塞建议频率
VACUUM标记可重用,不归还磁盘弱锁(可并发读写)❌ 不阻塞自动/每天
VACUUM FULL完全重写表 → 归还磁盘ACCESS EXCLUSIVE(排他)✅ 阻塞所有操作极少用,业务低峰期

大表 VACUUM FULL 是核武器——必须确认停机窗口。更安全的方式是用 pg_repack(在线重组,不锁表)。

事务 ID 回卷(XID Wraparound)

-- PG 用 32 位事务 ID,42 亿上限 → 需要循环使用
-- 风险查询:
SELECT datname, age(datfrozenxid) AS xid_age
FROM pg_database
WHERE age(datfrozenxid) > 1000000000   -- 超过 10 亿 → 告警
ORDER BY xid_age DESC;

-- 手动执行 FREEZE(需停机或低负载)
VACUUM FREEZE;

-- 设置告警阈值(建议监控)
-- age > 2^31(约 20 亿) → PG 拒绝新事务 → 灾难

索引类型速查

索引适用场景示例
B-Tree等值/范围查询(默认)WHERE id = 1, WHERE age BETWEEN 20 AND 30
Hash仅等值查询WHERE email = 'x@y.com'
GiST全文搜索/几何数据WHERE document @@ to_tsquery('search')
GIN数组/JSONB/全文索引WHERE tags @> ARRAY['tag1'], WHERE data->>'key' = 'value'
BRIN顺序极大的大表(如日志)WHERE created_at > '2026-01-01' — 块级索引
SP-GiST非平衡数据结构(IP/点)WHERE ip <<= '192.168.1.0/24'

JSONB 索引(PG 杀手级特性)

-- GIN 索引 JSONB 列
CREATE INDEX idx_data ON orders USING GIN (data jsonb_path_ops);

-- 查询示例(走索引)
SELECT * FROM orders WHERE data @> '{"status": "paid"}';
SELECT * FROM orders WHERE data->>'user_id' = '100';

关键参数调优

# 内存
shared_buffers = 4GB               # 物理内存的 25%(PG 依赖 OS 缓存)
effective_cache_size = 12GB        # 物理内存的 75%(优化器估算用)
work_mem = 64MB                    # 单个操作的排序/Hash 内存(需基于并发量调整)
maintenance_work_mem = 1GB         # VACUUM/REINDEX 内存

# WAL
wal_level = replica                # 流复制所需级别
max_wal_size = 8GB                 # WAL 最大大小(自动 Checkpoint)
checkpoint_timeout = 15min         # Checkpoint 间隔
wal_compression = on               # WAL 压缩(IO 密集型建议开启)

# 查询计划
random_page_cost = 1.1             # SSD 环境设为 1.0-1.5(默认 4 针对 HDD)
effective_io_concurrency = 200     # SSD 并发 IO(HDD 设为 2)

常用运维命令

-- 查看连接数
SELECT count(*), state FROM pg_stat_activity GROUP BY state;

-- KILL 特定连接
SELECT pg_terminate_backend(pid) FROM pg_stat_activity
WHERE state = 'idle in transaction' AND now() - state_change > interval '10 minutes';

-- 查看表大小
SELECT relname, pg_size_pretty(pg_total_relation_size(relid)) AS size
FROM pg_stat_user_tables ORDER BY pg_total_relation_size(relid) DESC LIMIT 10;

-- 查看慢查询(需先安装 pg_stat_statements)
CREATE EXTENSION pg_stat_statements;
SELECT query, calls, mean_exec_time, total_exec_time
FROM pg_stat_statements ORDER BY total_exec_time DESC LIMIT 10;

-- 查看死元组比例
SELECT relname,
       n_live_tup, n_dead_tup,
       round(100.0 * n_dead_tup / NULLIF(n_live_tup + n_dead_tup, 0), 2) AS dead_pct
FROM pg_stat_user_tables
WHERE n_dead_tup > 0 ORDER BY dead_pct DESC LIMIT 10;

常见问题 / 坑点

问题原因解决方案
连接数打满多进程模型 + 无连接池部署 PgBouncer(必须)
表膨胀,查询越来越慢VACUUM 跟不上 / 长事务阻止 VACUUM检查长事务 + 调整 autovacuum 参数
事务 ID 回卷告警VACUUM FREEZE 未执行紧急执行 VACUUM FREEZE + 监控 age
UPDATE 性能差MVCC 实现:每次 UPDATE = INSERT + DELETE批量更新分批提交 + 考虑分区表
COUNT(*)需扫描所有元组(不像 MySQL InnoDB 有优化)用估算 reltuples 或用 Redis 计数
索引膨胀频繁 UPDATE 导致旧索引项残留REINDEX CONCURRENTLY

关联知识

参考资源

学习时间

阶段时间备注
初次学习2026-07-14MVCC + VACUUM + 索引 + 连接池

状态: 📖 已掌握 下次复习日期: 2026-08-14