主题
性能调优:先定位等待,再动参数
一句话:参数只能放大或限制已有计划的质量;先找到慢在扫描、排序、锁还是连接,再改对应的那一项。
适用 PostgreSQL 16/17。默认值以官方文档为准;改参数前先记录基线。
为什么需要
PostgreSQL 的默认配置偏保守:shared_buffers 只有 128MB,work_mem 只有 4MB,max_connections 只有 100。这些默认值是为了让数据库在任何机器上都能启动,不是为生产负载调的。
但调参不是越多越好。work_mem 是每个排序/哈希操作的上限,调大后并发一高就可能 OOM;shared_buffers 超过物理内存的合理比例,会挤占页缓存。真正有效的顺序是:先看慢 SQL 和等待事件,再决定改哪个参数。
核心机制
四层内存
shared_buffers:PostgreSQL 自己的共享缓存,所有连接共享。默认 128MB(PostgreSQL 16/17),常见起点是物理内存的 25%。effective_cache_size:告诉优化器「操作系统页缓存大概有多大」,不分配内存,只影响代价估算。默认 4GB。work_mem:每个排序、哈希、去重操作的上限,不是每个查询。默认 4MB。一个查询有 3 个 Sort/Hash,就可能占 3 份。maintenance_work_mem:VACUUM、CREATE INDEX、ALTER TABLE 等维护操作的内存。默认 64MB。
代价模型
优化器用 seq_page_cost=1.0、random_page_cost=4.0、cpu_tuple_cost=0.01 估算计划成本。SSD 的随机读远没有 HDD 那么贵,random_page_cost 默认 4.0 会让优化器低估索引扫描、偏向全表扫描。常见做法是调到 1.1–1.5,然后用 EXPLAIN 对比计划,不要一次改多个参数。
连接模型
PostgreSQL 每个连接一个后端进程。连接越多,内存和进程切换成本越高。生产通常用 PgBouncer 的 pool_mode=transaction,把大量应用连接复用到少量数据库连接。
每连接内存没有固定值。不要用「4–8MB」或「约 10MB」这类口径做容量规划;在目标会话里执行:
sql
-- PostgreSQL 14+;在要测量的会话中执行
SELECT sum(total_bytes) / 1024 / 1024 AS mb
FROM pg_backend_memory_contexts;先跑一段典型查询,再测当前后端的内存上下文,得到的值才有意义。
参数表
| 参数=默认值(版本) | 设置命令 | 影响面 |
|---|---|---|
shared_buffers=128MB(16/17) | ALTER SYSTEM SET shared_buffers='8GB'; 重启 | 共享缓存;过大会挤占 OS 页缓存 |
effective_cache_size=4GB(16/17) | ALTER SYSTEM SET effective_cache_size='24GB'; 重载 | 只影响优化器估算,不分配内存 |
work_mem=4MB(16/17) | SET work_mem='16MB'; | 每个 Sort/Hash 的上限;按并发数估算总内存 |
maintenance_work_mem=64MB(16/17) | SET maintenance_work_mem='1GB'; | VACUUM/建索引;只影响当前会话 |
max_connections=100(16/17) | ALTER SYSTEM SET max_connections=300; 重启 | 连接上限;每连接都有内存成本 |
random_page_cost=4.0(16/17) | SET random_page_cost=1.1; | SSD 上偏高会让优化器偏向全表扫描 |
seq_page_cost=1.0(16/17) | 通常不改 | 顺序读的基准成本 |
wal_buffers=-1(16/17,自动) | ALTER SYSTEM SET wal_buffers='16MB'; 重启 | WAL 缓冲;高写入可适当加大 |
checkpoint_timeout=5min(16/17) | ALTER SYSTEM SET checkpoint_timeout='15min'; 重载 | 检查点间隔;拉长可减少刷盘抖动,但恢复变慢 |
max_wal_size=1GB(16/17) | ALTER SYSTEM SET max_wal_size='8GB'; 重载 | 触发检查点的 WAL 上限;越大检查点越少 |
autovacuum_max_workers=3(16/17) | ALTER SYSTEM SET autovacuum_max_workers=6; 重启 | 并发 autovacuum 数;大表多时调大 |
autovacuum_vacuum_scale_factor=0.2(16/17) | ALTER TABLE big SET (autovacuum_vacuum_scale_factor=0.01); | 死元组触发比例;大表按表调小 |
log_min_duration_statement=-1(16/17) | ALTER SYSTEM SET log_min_duration_statement=1000; 重载 | 记录超过 1 秒的 SQL;默认关闭 |
track_io_timing=off(16/17) | ALTER SYSTEM SET track_io_timing=on; 重载 | 让 EXPLAIN (BUFFERS) 显示 IO 时间;有少量开销 |
jit=on(16/17) | SET jit=off; | 短查询可能因编译开销变慢;长分析查询通常受益 |
max_parallel_workers_per_gather=2(16/17) | SET max_parallel_workers_per_gather=4; | 单条查询的并行 worker;小查询不要开 |
改完用 SELECT pg_reload_conf(); 让 SIGHUP 类参数生效;shared_buffers、max_connections、wal_buffers、autovacuum_max_workers 需要重启。
最小上手
sql
-- 需要 shared_preload_libraries='pg_stat_statements' 并重启
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
-- 总耗时最高的 SQL(PostgreSQL 13+ 用 total_exec_time)
SELECT queryid,
calls,
round(total_exec_time::numeric, 1) AS total_ms,
round(mean_exec_time::numeric, 2) AS mean_ms,
round(100.0 * shared_blks_hit
/ NULLIF(shared_blks_hit + shared_blks_read, 0), 2) AS hit_pct,
left(query, 80) AS query
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 10;sql
-- 当前活跃会话与等待事件
SELECT pid,
now() - xact_start AS xact_age,
wait_event_type,
wait_event,
state,
left(query, 80) AS query
FROM pg_stat_activity
WHERE state <> 'idle'
ORDER BY xact_age DESC
LIMIT 20;sql
-- 死元组比例与最近一次 autovacuum
SELECT relname,
n_live_tup,
n_dead_tup,
round(100.0 * n_dead_tup
/ GREATEST(n_live_tup + n_dead_tup, 1), 2) AS dead_pct,
last_autovacuum
FROM pg_stat_user_tables
ORDER BY n_dead_tup DESC
LIMIT 20;PgBouncer 的最小配置:
ini
[pgbouncer]
listen_port = 6432
pool_mode = transaction
max_client_conn = 2000
default_pool_size = 50
reserve_pool_size = 10
server_idle_timeout = 600transaction 模式下,会话级特性要小心:SET、LISTEN/NOTIFY、会话级 advisory lock、部分 prepared statement 用法可能在连接复用后失效。需要跨语句保持状态时,用事务级机制或单独的 session 池。
生产动作
按「现象 → 命令/指标 → 判断」排查:
- CPU 高、查询慢:
pg_stat_statements找total_exec_time最高的 SQL →EXPLAIN (ANALYZE, BUFFERS)→ 看Seq Scan、Rows Removed by Filter、连接方式。 - 排序慢:计划里出现
Sort Method: external merge→work_mem不够或并发太高 → 先按查询调SET LOCAL work_mem,不要全局拉满。 - 缓存命中低:
Buffers: shared read很多、hit_pct下降 → 数据量超过内存或冷数据多 → 评估shared_buffers、数据分区、只读副本。 - 表/索引膨胀:
n_dead_tup/n_live_tup超过 20%、last_autovacuum很久以前 → autovacuum 追不上 → 按表调小scale_factor、增加 worker,必要时pg_repack(不要高峰做VACUUM FULL,它拿ACCESS EXCLUSIVE)。 - 连接打满:
pg_stat_activity按state计数接近max_connections→ 连接泄漏或池配置错误 → 修池,不要只调大max_connections。 - 锁等待:
wait_event_type='Lock'、pg_blocking_pids(pid)非空 → 看 12 篇;先找阻塞者,不要直接pg_terminate_backend。 - 事务 ID 回卷余量:
SELECT age(datfrozenxid) FROM pg_database;接近autovacuum_freeze_max_age=200000000→ 立即安排VACUUM FREEZE,不要等只读保护。 - 参数生效方式:
ALTER SYSTEM写入postgresql.auto.conf;SIGHUP 类用SELECT pg_reload_conf();,postmaster 类必须重启。改完用SHOW 参数名;确认。
边界
- 调参不能修复坏 SQL/坏索引:先看计划,再改参数;
work_mem再大也救不了全表扫描。 work_mem不是查询级:一个查询多个 Sort/Hash 各占一份,并发 100、每个 3 个 Sort、work_mem=64MB,峰值可能到 19GB。shared_buffers不是越大越好:超过物理内存的合理比例会挤占 OS 页缓存;还要给连接、维护操作留内存。effective_cache_size不分配内存:它只是优化器的假设;设得离谱会让计划判断失真。random_page_cost依赖存储:HDD 和 SSD、云盘和本地 NVMe 差异很大;改之前先跑基准,改之后看计划。- 不要关闭 autovacuum:它同时负责死元组清理和事务 ID 冻结;关掉只是把问题推迟到更严重。
- PgBouncer 事务模式不是透明代理:会话状态、advisory lock、prepared statement 都可能有兼容问题;上线前用真实驱动压测。
讲清楚
PostgreSQL 调参的核心是分清四层内存:shared_buffers 是共享缓存,effective_cache_size 只是给优化器的假设,work_mem 是每个排序/哈希操作的上限,maintenance_work_mem 管 VACUUM 和建索引。最常见的错误是把 work_mem 当成查询级、把 effective_cache_size 当成实际分配,或者用固定值估算每连接内存。正确顺序是先用 pg_stat_statements 和 pg_stat_activity 找到慢 SQL 与等待事件,再看 EXPLAIN (ANALYZE, BUFFERS) 判断是扫描、排序、锁还是连接问题,最后才改对应参数。连接数要靠池解决,表膨胀要靠 autovacuum 和 pg_repack 解决,事务 ID 回卷要靠 age(datfrozenxid) 提前监控。参数是放大器,不是修复器。