在 《PostgreSQL vs SQL Server:语法差异与迁移避坑指南》 中,我们对比了两款数据库的特性差异。但无论用哪款,慢查询都是压垮性能的罪魁祸首。本文聚焦 PostgreSQL,教你用 EXPLAIN ANALYZE 定位瓶颈、用索引和统计信息把慢 SQL 提速 10 倍以上。
一、先学会看执行计划
优化 SQL 的第一步不是改代码,而是看懂数据库怎么执行的。PostgreSQL 提供 EXPLAIN ANALYZE 命令,它会真实执行 SQL 并返回每一步的耗时和行数:
EXPLAIN ANALYZE
SELECT * FROM orders
WHERE user_id = 12345
ORDER BY created_at DESC
LIMIT 20;
输出里的关键信号:
- Seq Scan(全表扫描):逐行扫描整张表,数据量大时必慢。→ 需要加索引
- rows=1000 actual=1:估算行数与实际偏差巨大,说明统计信息过期 → 需要
ANALYZE - cost 数值极高:整体代价高,重点关注最外层的总 cost
- Buffers: shared hit/read:read 多说明频繁读磁盘,缓存不足
二、索引:最常见的加速器
2.1 B-tree 索引(默认)
针对 WHERE 和 ORDER BY 的字段建索引:
-- 单列索引
CREATE INDEX idx_orders_user ON orders(user_id);
-- 复合索引(注意最左前缀:先等值后范围/排序)
CREATE INDEX idx_orders_user_created ON orders(user_id, created_at DESC);
⚠️ 最左前缀原则:(user_id, created_at) 的索引能加速 WHERE user_id=? 和 WHERE user_id=? ORDER BY created_at,但不能单独加速 ORDER BY created_at(跳过了最左列)。
2.2 覆盖索引(Index-Only Scan)
如果查询的字段都在索引里,PostgreSQL 无需回表,性能再次翻倍:
-- INCLUDE 把额外列放进索引(PG 11+)
CREATE INDEX idx_orders_cover
ON orders(user_id)
INCLUDE (status, total_amount);
三、统计信息过期:隐形的性能杀手
PostgreSQL 优化器依赖统计信息(pg_statistic)来估算行数。如果表经历过大量增删,统计信息失真,优化器可能选错执行计划(比如本该走索引却走了全表扫描)。
-- 更新单表统计信息
ANALYZE orders;
-- 查看统计信息是否过期(n_dead_tup 远大于 n_live_tup 说明需要)
SELECT schemaname, relname, n_live_tup, n_dead_tup
FROM pg_stat_user_tables
WHERE n_dead_tup > 1000
ORDER BY n_dead_tup DESC;
建议把 autovacuum 保持开启(默认开启),对大表可适当调高 default_statistics_target 提升估算精度。
四、实战案例:从 3.2s 到 80ms
一个真实慢查询:
-- 优化前:3.2s
SELECT o.*, u.name
FROM orders o
JOIN users u ON o.user_id = u.id
WHERE o.status = 'PAID'
AND o.created_at >= '2026-01-01'
ORDER BY o.created_at DESC
LIMIT 50;
EXPLAIN ANALYZE 显示对 orders 走了 Seq Scan,cost 高达 82000。优化步骤:
- 建复合索引:
CREATE INDEX idx_orders_status_created ON orders(status, created_at DESC); - 更新统计:
ANALYZE orders; - 复测:执行计划变为 Index Scan,cost 降到 420,耗时 80ms。
五、进阶优化清单
| 手段 | 适用场景 | 预期收益 |
|---|---|---|
| 复合索引 | 多条件查询 | 10-100x |
| 覆盖索引 | 只读少量列 | 2-5x |
| 部分索引 | 只查某状态子集 | 索引体积减半 |
| 分区表 | 超亿行大表 | 查询裁剪 |
| 物化视图 | 复杂聚合报表 | 避免重复计算 |
| 连接池 | 高并发短连接 | 降低建连开销 |
其中部分索引很实用——如果 90% 查询只看 status='PAID',建 CREATE INDEX ... WHERE status='PAID' 可大幅缩小索引体积:
CREATE INDEX idx_orders_paid
ON orders(created_at DESC)
WHERE status = 'PAID';
总结
PostgreSQL 慢查询优化的标准套路:EXPLAIN ANALYZE 看计划 → 加合适的索引(复合/覆盖/部分)→ ANALYZE 更新统计 → 复测对比。记住,索引不是越多越好——每个索引都会拖慢写入,要为真实查询模式服务。
如果你的表已经超过千万行,下一篇我们可以聊聊分区表与连接池(PgBouncer)的实战。有慢 SQL 卡住?评论区贴出来一起分析 👇




