
上次写了 PostgreSQL 安装部署(文章 #25),这次把性能优化这块补上。从配置调优到索引设计,从 SQL 优化到日常运维,一篇文章帮你把 PG 的性能潜力榨干。
PostgreSQL 性能优化可以分成几个层次,从下往上依次影响:
| 层次 | 优化方向 | 提升效果 |
|---|---|---|
| 🖥️ 硬件 & OS | CPU、内存、磁盘、内核参数 | ⭐⭐⭐⭐⭐ |
| ⚙️ 配置参数 | shared_buffers、work_mem 等 | ⭐⭐⭐⭐ |
| 📊 索引设计 | B-tree、GIN、BRIN、部分索引 | ⭐⭐⭐⭐ |
| 📝 SQL 优化 | 查询重写、JOIN 优化 | ⭐⭐⭐ |
| 🔄 日常维护 | VACUUM、ANALYZE、统计信息 | ⭐⭐⭐ |
| 📦 架构层面 | 连接池、读写分离、分区表 | ⭐⭐⭐⭐⭐ |
最重要的一条原则: 不要凭感觉调优,先用数据说话。一切优化的起点都是 EXPLAIN ANALYZE。
在开始调优之前,先找到哪些查询在拖后腿:
-- 查看当前正在运行的查询
SELECT pid, now() - pg_stat_activity.query_start AS duration,
query, state
FROM pg_stat_activity
WHERE state != 'idle'
ORDER BY duration DESC;
-- 查看执行时间最长的历史查询
SELECT query, calls, total_exec_time, mean_exec_time,
rows, shared_blks_hit, shared_blks_read
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 10;
⚠️ 注意:
pg_stat_statements是个扩展,需要先安装:CREATE EXTENSION IF NOT EXISTS pg_stat_statements;并在
postgresql.conf中加上shared_preload_libraries = 'pg_stat_statements',然后重启。
找到慢查询后,用 EXPLAIN ANALYZE 看它到底慢在哪:
EXPLAIN (ANALYZE, BUFFERS, TIMING)
SELECT * FROM orders WHERE created_at > '2026-01-01' ORDER BY total_amount DESC;
输出中的几个关键信号:
| 看到这个 | 说明 | 怎么办 |
|---|---|---|
Seq Scan 在大表上 |
全表扫描,没有走索引 | 加索引 |
Sort Method: external merge |
排序内存不够,用了磁盘 | 增大 work_mem |
actual time=0.05..850 |
启动快但总时间长 | 看是否有大量随机 I/O |
rows=10 vs loops=1000 |
估计行数严重偏离实际 | 更新统计信息 |
Buffers: shared hit=5 read=10000 |
大量读磁盘而不是命中缓存 | 增大 shared_buffers |
-- 缓存命中率(低于 99% 说明 shared_buffers 可能不够)
SELECT 'index hit rate' AS name,
(sum(idx_blks_hit)) / nullif(sum(idx_blks_hit + idx_blks_read), 0) * 100 AS ratio
FROM pg_statio_user_indexes
UNION ALL
SELECT 'table hit rate',
(sum(heap_blks_hit)) / nullif(sum(heap_blks_hit + heap_blks_read), 0) * 100
FROM pg_statio_user_tables;
-- 表膨胀率
SELECT schemaname, tablename, n_dead_tup, n_live_tup,
round(n_dead_tup * 100 / nullif(n_live_tup, 0), 2) AS dead_pct
FROM pg_stat_user_tables
WHERE n_live_tup > 0
ORDER BY dead_pct DESC;
-- 未使用索引
SELECT schemaname, tablename, indexname, idx_scan
FROM pg_stat_user_indexes
WHERE idx_scan = 0 AND indexrelid NOT IN (
SELECT indexrelid FROM pg_constraint WHERE conindid = indexrelid
);
以下是一台 16GB 内存、8核 CPU、SSD 存储 机器的参考配置:
# === 内存配置(最重要的参数) ===
shared_buffers = 4GB # RAM 的 25%,不要超过 8GB
effective_cache_size = 12GB # RAM 的 50%~75%
work_mem = 64MB # 默认 4MB 太小,但并发高时别太大
maintenance_work_mem = 1GB # VACUUM/索引维护用
wal_buffers = 64MB # 写密集型再加大
# === 优化器 ===
random_page_cost = 1.1 # SSD 设 1.1,HDD 保持默认 4
effective_io_concurrency = 200 # SSD 设 200,HDD 设 2
default_statistics_target = 500 # 默认 100,统计信息更精确
# === 连接 ===
max_connections = 200 # 别设太大,推荐连接池方案
superuser_reserved_connections = 10
# === WAL / 检查点 ===
wal_level = replica
max_wal_size = 4GB
min_wal_size = 1GB
checkpoint_completion_target = 0.9
wal_compression = zstd # PG 18 支持 zstd 压缩 WAL
# === 自动维护 ===
autovacuum = on
autovacuum_max_workers = 4
autovacuum_naptime = 30s
autovacuum_vacuum_scale_factor = 0.01 # 默认 0.2 太大,改小
autovacuum_vacuum_threshold = 50
懒得手动算?用 PGTune 在线生成:
填上你的服务器规格(内存、CPU、连接数、存储类型),它自动给出推荐配置,比手动估算靠谱得多。
不要一次改一堆参数! 每次只改 1~2 个,重启后通过 EXPLAIN ANALYZE 对比前后差异。
# 查看当前配置
psql -c "SHOW ALL;" | grep -E 'shared_buffers|work_mem|effective_cache'
# 热加载配置(不需要重启的部分)
SELECT pg_reload_conf();
# 查看当前运行时的实际使用量
SELECT pg_size_pretty(sum(pg_database_size(datname))) AS total_size
FROM pg_database;
PostgreSQL 支持 6 种索引类型,选对了事半功倍:
| 索引类型 | 适用场景 | 一句话总结 |
|---|---|---|
| B-tree(默认) | 等值查询、范围查询、排序 | 90% 的情况用这个就够了 |
| Hash | 等值查询 | 比 B-tree 略快,但不能范围查询 |
| GIN | 数组、全文检索、JSONB | 适合"包含"类型查询 |
| GiST | 全文检索、地理位置 | 适合模糊搜索和距离排序 |
| BRIN | 超大数据表、物理有序数据 | 索引极小(比 B-tree 小 100 倍) |
| SP-GiST | 空间数据、分区数据 | 适合不均匀分布的数据 |
覆盖索引(Index-Only Scan):
-- 如果只查 amount 和 status,把这两个字段也放进索引
CREATE INDEX idx_orders_covering ON orders(created_at) INCLUDE (amount, status);
这样查询不需要回表,直接走索引就能拿到全部数据。
部分索引(Partial Index):
-- 只索引"未处理"的订单,索引体积缩小 90%
CREATE INDEX idx_orders_pending ON orders(created_at)
WHERE status = 'pending';
适合低频值查询的场景——只索引你想要的那部分数据。
复合索引的字段顺序:
-- 等值条件放前面,范围条件放后面
CREATE INDEX idx_orders_lookup ON orders(status, created_at DESC);
经验法则:
WHERE a = 1 AND b = 2 → 索引 (a, b),顺序无所谓WHERE a = 1 AND b > 10 → 索引 (a, b),等值在前,范围在后WHERE a = 1 ORDER BY b → 索引 (a, b),把排序列放在最后-- 查看索引使用率
SELECT schemaname, tablename, indexname, idx_scan, idx_tup_read, idx_tup_fetch
FROM pg_stat_user_indexes
ORDER BY idx_scan ASC;
-- 重建索引(PG 18 支持 CONCURRENTLY 重建,不锁表)
REINDEX INDEX CONCURRENTLY idx_orders_created_at;
定期检查零扫描的索引——如果一个索引长期没用,果断删掉,别让它拖慢写入。
-- 慢!精确计数大表很慢
SELECT COUNT(*) FROM orders;
-- 快!用 pg_class 估算(误差可接受)
SELECT reltuples::bigint AS estimated_count
FROM pg_class WHERE relname = 'orders';
-- 更快!用事务 ID 范围近似
SELECT txid_current();
-- ❌ 传统的 OFFSET 分页,越翻越慢
SELECT * FROM orders ORDER BY id LIMIT 20 OFFSET 100000;
-- ✅ 游标分页(Keyset Pagination),翻到天荒地老也一样快
SELECT * FROM orders
WHERE id > 100000
ORDER BY id
LIMIT 20;
-- 用 EXISTS 替代 IN(当子表很大时)
-- ❌ 慢
SELECT * FROM users WHERE id IN (SELECT user_id FROM orders);
-- ✅ 快(只要找到一条就停止)
SELECT * FROM users WHERE EXISTS (SELECT 1 FROM orders WHERE orders.user_id = users.id);
-- 不要一条一条 INSERT
INSERT INTO orders (user_id, amount, created_at) VALUES
(1, 100, NOW()),
(2, 200, NOW()),
(3, 300, NOW());
-- 大数据量导入用 COPY
COPY orders FROM '/tmp/orders.csv' WITH (FORMAT CSV, HEADER true);
PostgreSQL 的 MVCC 机制会导致"死元组"堆积,VACUUM 就是清理它们的:
-- 查看需要 VACUUM 的表
SELECT schemaname, tablename, n_dead_tup, last_autovacuum
FROM pg_stat_user_tables
WHERE n_dead_tup > 1000
ORDER BY n_dead_tup DESC;
-- 手动 VACUUM 指定表
VACUUM ANALYZE orders;
关键点: 现在 autovacuum 默认开启,一般不需要手动操作。但如果你发现某张大表频繁更新删除,可能需要调小 autovacuum_vacuum_scale_factor。
-- 全库 ANALYZE
ANALYZE;
-- 指定表,更精确
ANALYZE orders;
-- 按大小排序
SELECT relname, pg_size_pretty(pg_total_relation_size(oid)) AS total_size
FROM pg_class
WHERE relkind = 'r' AND relnamespace = (
SELECT oid FROM pg_namespace WHERE nspname = 'public'
)
ORDER BY pg_total_relation_size(oid) DESC;
PostgreSQL 的进程模型决定了:连接越多越慢。每个连接都是一个独立进程,200 个连接就是 200 个进程在抢 CPU。
推荐用 PgBouncer 做连接池:
# 安装
sudo apt install pgbouncer
# 最小配置
[databases]
mydb = host=127.0.0.1 port=5432 dbname=mydb
[pgbouncer]
listen_port = 6432
pool_mode = transaction # 事务级复用,最推荐
max_client_conn = 500
default_pool_size = 20 # 实际到 PG 的连接数
对于超过千万级的表,分区可以显著提升查询和 DML 性能:
-- PG 18 支持原生分区
CREATE TABLE orders_partitioned (
id BIGSERIAL,
created_at TIMESTAMPTZ NOT NULL,
amount NUMERIC(10,2)
) PARTITION BY RANGE (created_at);
-- 按月分区
CREATE TABLE orders_2026_q3 PARTITION OF orders_partitioned
FOR VALUES FROM ('2026-07-01') TO ('2026-10-01');
PG 18 的并行查询已经相当成熟:
-- 查看并行配置
SHOW max_parallel_workers_per_gather;
SHOW max_parallel_workers;
-- 让大查询自动并行(通常不需要干预)
SET max_parallel_workers_per_gather = 4;
对于大表上的聚合查询和 JOIN,并行查询能自动利用多核 CPU。
| 工具 | 用途 | 一句话 |
|---|---|---|
| pg_stat_statements | 内置慢查询追踪 | 必装扩展 |
| pgBadger | 日志分析生成 HTML 报告 | 看日志趋势 |
| PgTune | 配置参数计算器 | 不用自己算 |
| pg_activity | 终端版 top 监控 | 实时看查询 |
| pgMustard | EXPLAIN 可视化分析 | 收费但好用 |
可能是因为 OS 缓存被挤占了。共享缓冲区(shared_buffers)和 OS 页面缓存是两级缓存,shared_buffers 占太多挤占了 OS 缓存的空间,总缓存命中率反而下降。建议 shared_buffers 不超过 RAM 的 40%。
不是。work_mem 是每个查询操作可以使用的内存。如果设 1GB,同时有 50 个查询在做排序,就是 50GB——内存直接爆炸。64MB~256MB 是比较平衡的区间。
尽量不要在生产环境用。VACUUM FULL 会锁表并重建,导致该表完全不可用。PG 18 中有更温和的 pg_repack 扩展,可以在不锁表的情况下回收空间。
PostgreSQL 性能优化不是玄学,有章可循:
EXPLAIN ANALYZE 和 pg_stat_statements 找到问题最后送一句话:PostgreSQL 的默认配置只保证能跑,不保证跑得快。 花半小时调一下参数,可能比升级硬件划算得多。
本文由老张整理编写,信息综合自 PostgreSQL 官方文档、PGTune 社区实践及个人实战经验。
📖 上一篇:PostgreSQL 安装部署实战:从零搭建生产级环境 — 如果还没装 PG,先看这篇。