先让慢查询现形:打开慢日志并设阈值
调优的前置不是猜哪条重要,而是把"哪些查询慢"客观捞出来。PostgreSQL 的 log_min_duration_statement 会在执行超过阈值后把完整语句打印进日志;配合 log_duration,把耗时长但没超阈值的语句也纳进来。
对生产库别把阈值设成 0(那会写入海里日志),从 2000ms 起步,分析优化后再逐步下调到合理水平;修改方式既可以用 ALTER SYSTEM 持久化,也可以按会话 SET 设个别查询监控。
ALTER SYSTEM SET log_min_duration_statement = 2000;
ALTER SYSTEM SET log_duration = on;
SELECT pg_reload_conf();
# 注册一次日志目录并读
SHOW log_directory;
-- 按会话只监本连接
SET log_min_duration_statement = 500;读懂 EXPLAIN:Seq Scan v.s. Index Scan 与 rows 预估
拿到一条慢查询,用 EXPLAIN (ANALYZE, BUFFERS) 看执行计划。重点有一个:rows 预估是否偏离实际 rows 太大。若 planner 预计 100 行实际 10 万行,多半是统计信息过期,VACUUM ANALYZE 或手动 ANALYZE 后就可能有巨大改观。
当看到大表上的 Seq Scan 时,先用覆盖率判断:返回超过表行数约 10% 的查询,Seq Scan 有时反而比索引更快,此时盲目加索引无益;低于该比例却还是 Seq Scan,才考虑建索引。这是一句能在多数场景直接指导决策的判据。
EXPLAIN (ANALYZE, BUFFERS, VERBOSE) SELECT * FROM orders WHERE buyer_id = 42 AND created_at > now() - interval '7 days';
-- 看 rows 预估 vs actual
-- 若 stats 过期:
ANALYZE orders;
CREATE INDEX idx_orders_buyer_created ON orders(buyer_id, created_at DESC);
EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM orders WHERE buyer_id = 42 AND created_at > now() - interval '7 days';统计与膨胀:index 建对了却不生效的两大隐藏原因
有时索引明明存在,EXPLAIN 却依旧 Seq Scan。除了 selectivity 过低判据,两大隐藏原因值得先查:一是统计信息过期;二是表/索引膨胀(dead tuples 堆积)。后者会让 planner 看到高到了离谱的 page 数,于是认定全表扫描更划算。
处理是持续维护:autovacuum 默认开着,但频繁 UPDATE 的表容易追不上膨胀。要核实 autovacuum 有没有做,查 pg_stat_user_tables 的 last_autovacuum 与 vacuum_count;设置 target 参数并按表加大 autovacuum_vacuum_scale_factor 或打开其 index bloat 监控。
SELECT relname, n_live_tup, n_dead_tup, last_autovacuum, last_analyze
FROM pg_stat_user_tables ORDER BY n_dead_tup DESC LIMIT 10;
-- 强制做一次标志性维护
VACUUM (ANALYZE, VERBOSE) orders;
-- 对高更新表放大 vacuum 触发
ALTER TABLE orders SET (autovacuum_vacuum_scale_factor = 0.02);参数调优第一梯队:shared_buffers、work_mem 与 effective_cache_size
PostgreSQL 默认参数偏保守,通常是给通用小机器配的。三个参数提升最省事:shared_buffers 一般建议机器物理内存的 25% 左右(封顶合理值),work_mem 管排序与哈希(按会话算内存,太大并发下会爆),effective_cache_size 告诉优化器可信的文件系统缓存总容量(约物理内存 75%)。
调整要用可复算的公式而非拍脑袋:这边给出按 G 内存的入门公式。改 important 参数前确认 postgresql.conf 路径,并记得 SHOW config 验证实际生效值,改 server 参数要重启,session 参数才可动态重载。
# 8C/16G 机器的示例后设
shared_buffers = 4GB # 16GB * 25%
work_mem = 64MB # 每条排序/哈希作业最多
effective_cache_size = 12GB # 16GB * 75%
maintenance_work_mem = 1GB # 供 VACUUM/重建索引
# 查生效值
SHOW shared_buffers; SHOW work_mem; SHOW effective_cache_size;连接池:max_connections 调高不是免费午餐
把 max_connections 无脑调到几千是常见错误:每个接人都要分一份栈内存和进程开销,线程阻塞在锁上,吞吐反而断崖下降。PostgreSQL 每连接是独立进程(process),负载上来后果比预想更重。
真实解法是把流向数据库的连接收敛到连接池(PgBouncer),并保持数据库侧 max_connections 在一个合理值(比如 100~300)。判断连接是否过载,用 pg_stat_activity 看堆积的 idle in transaction 以及等待的草态。下面给出合理的防线。
SELECT state, count(*) FROM pg_stat_activity GROUP BY state ORDER BY 2 DESC;
-- 识别卡住的 import 事务
SELECT pid, now()-query_start AS age, state, left(query,60) FROM pg_stat_activity WHERE state IN (\'active\',\'idle in transaction\') ORDER BY age DESC LIMIT 10;
-- 通知连接池而不是硬扩数据库
ALTER SYSTEM SET max_connections = 300;
SELECT pg_reload_conf();错误示范与修复:work_mem 开满导致的级联慢
把 work_mem 直接设成 2GB 以为能加速排序,结果并发 30 个 session 的排序作业会瞬间吃掉 60GB 内存,swap 一起可能整库瘫痪。这类"单个参数加速"若不在并发维度核算,反而拖垮全库。
正确姿势是把排序/索引过程迁到专门通道(maintenance_work_mem)预留额度给 VACUUM,或按应用会话单独 SET work_mem,绝不全局粗暴调高。给出错误示范与修复对照,以及如何验证内存安全。
# 错误示范:全局设超大 work_mem
# work_mem = 2GB
# 修复对照:按会话/作业细分
work_mem = 64MB
maintenance_work_mem = 1GB
-- 特定大排序会话单独抬一点
SET work_mem = '256MB';
-- 验证没有 swap 挤出引用计数
SELECT name, setting FROM pg_settings WHERE name IN ('work_mem','maintenance_work_mem');
SELECT pg_size_pretty(shared_buffers) FROM pg_settings WHERE name='shared_buffers';