一个慢查询从 8 秒优化到 50 毫秒的过程。本文通过一个真实案例,讲解 EXPLAIN ANALYZE 的使用、索引策略以及常见的查询反模式。
| Node | Rows | Time | Method |
|---|---|---|---|
| Hash Join | 12,450 | 8,142ms | — |
| ├─ Seq Scan on orders | 847,203 | 3,210ms | 全表扫描 |
| ├─ Seq Scan on users | 125,000 | 1,890ms | 全表扫描 |
| └─ Sort | 12,450 | 2,450ms | External Merge |
一、问题背景
一个订单报表查询,关联 users 和 orders 两张表,按时间范围过滤并分页。数据量:users 12.5 万,orders 84.7 万。查询耗时 8 秒,用户投诉页面加载慢。
-- 原始慢查询
SELECT o.id, o.amount, o.created_at, u.name, u.email
FROM orders o
JOIN users u ON o.user_id = u.id
WHERE o.created_at BETWEEN '2026-01-01' AND '2026-03-31'
AND o.status = 'completed'
ORDER BY o.created_at DESC
LIMIT 20;
二、EXPLAIN ANALYZE 分析
执行 EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT) 后发现两个问题:
- orders 表全表扫描:没有对
created_at建索引 - Sort 使用 External Merge:排序数据超出
work_mem,溢出到磁盘
三、优化过程
3.1 添加复合索引
最关键的优化:创建一个覆盖 WHERE 条件和 ORDER BY 的复合索引。
-- 复合索引:覆盖过滤 + 排序
CREATE INDEX idx_orders_status_created
ON orders (status, created_at DESC)
WHERE status = 'completed'; -- 部分索引,更小更快
-- users 表主键已有索引,无需额外创建
💡 部分索引 (Partial Index):只索引
status = 'completed' 的记录,索引体积从 ~85MB 缩小到 ~25MB,查询速度更快。适用于大部分查询只针对特定状态的场景。
3.2 调整 work_mem
-- 会话级别调整(针对这个查询)
SET work_mem = '64MB';
-- 或者全局调整(谨慎)
ALTER SYSTEM SET work_mem = '32MB';
SELECT pg_reload_conf();
3.3 优化后的查询
-- 优化后:使用覆盖索引,避免回表
SELECT o.id, o.amount, o.created_at, u.name, u.email
FROM orders o
JOIN users u ON o.user_id = u.id
WHERE o.status = 'completed'
AND o.created_at BETWEEN '2026-01-01' AND '2026-03-31'
ORDER BY o.created_at DESC
LIMIT 20;
| Node | Rows | Time | Method |
|---|---|---|---|
| Nested Loop Join | 20 | 48ms | — |
| ├─ Index Scan Backward | 20 | 2ms | idx_orders_status_created |
| └─ Index Scan on users_pkey | 20 | 1ms | B-Tree |
四、优化效果
❌ 优化前
8,142
毫秒
✅ 优化后
48
毫秒
性能提升 169 倍。核心就两件事:加对索引 + 调 work_mem。
五、常用诊断命令
-- 查看慢查询(需要开启 pg_stat_statements)
SELECT query, mean_exec_time, calls
FROM pg_stat_statements
ORDER BY mean_exec_time DESC
LIMIT 10;
-- 查看缺失的索引建议
SELECT schemaname, tablename, attname, n_distinct, correlation
FROM pg_stats
WHERE tablename = 'orders'
AND n_distinct > 100;
-- 查看表膨胀
SELECT pg_size_pretty(pg_total_relation_size('orders'));
-- 查看索引使用率
SELECT indexrelname, idx_scan, idx_tup_read
FROM pg_stat_user_indexes
WHERE relname = 'orders';
六、总结
- 永远用
EXPLAIN ANALYZE分析慢查询,不要靠猜 - 复合索引的列顺序很重要:等值条件在前,范围条件在后
- 部分索引能大幅减小索引体积,提升查询速度
work_mem不够排序就会溢出磁盘,这是性能杀手- 定期检查
pg_stat_statements,主动发现慢查询