PostgreSQL慢查询现场拆解:用EXPLAIN、复合索引和VACUUM把接口从820ms压到37ms
第1章:OK so,先把慢查询抓出来,别凭感觉加索引
兄弟们开机!这里是 eccfy,今天我们直接上手拆一个 PostgreSQL 慢接口。屏幕左边是应用日志,右边是 psql,接口平均 820ms,目标:压到 50ms 内。接下来别急着建索引,先抓证据。
第一步,打开慢查询日志。我的测试环境是 PostgreSQL 15,表 orders 约 120 万行,单次查询按 user_id、status、created_at 过滤。
ALTER SYSTEM SET log_min_duration_statement = 200;ALTER SYSTEM SET log_statement = 'none';SELECT pg_reload_conf();
如果你在搜“PostgreSQL性能调优教程”,记住一个原则:先定位最慢、调用最多、返回行数异常的 SQL。再启用 pg_stat_statements:
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;SELECT query, calls, mean_exec_time, rows FROM pg_stat_statements ORDER BY mean_exec_time DESC LIMIT 5;
Now watch this!我抓到的 SQL 是:
SELECT * FROM orders WHERE user_id = 10086 AND status = 'paid' ORDER BY created_at DESC LIMIT 20;
执行计划看这里:
EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM orders WHERE user_id = 10086 AND status = 'paid' ORDER BY created_at DESC LIMIT 20;
结果显示 Seq Scan,扫描 120 万行,耗时 812ms,Buffers read 超过 9000。看到 Seq Scan 不一定错,但在高选择性条件下,就是典型慢点。
第2章:接下来建对索引,别把数据库变成“索引垃圾场”
很多人搜“PostgreSQL索引优化怎么用”,第一反应是给每个字段单独建索引:user_id 一个、status 一个、created_at 一个。停!这个查询需要的是复合索引,而且顺序很关键。
我们的过滤条件是 user_id 精确匹配、status 精确匹配,排序是 created_at DESC,所以直接建:
CREATE INDEX CONCURRENTLY idx_orders_user_status_created ON orders (user_id, status, created_at DESC);
注意我用了 CONCURRENTLY,线上建索引时它能减少写入阻塞,但会更慢。我这张 120 万行表建索引用了 11.6 秒,索引大小 68MB。
接下来重新跑执行计划:
EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM orders WHERE user_id = 10086 AND status = 'paid' ORDER BY created_at DESC LIMIT 20;
Before/After 来了:优化前 812ms,优化后 37ms;Buffers read 从 9000+ 降到 43。执行计划从 Seq Scan 变成 Index Scan using idx_orders_user_status_created。弹幕可以刷“起飞”。
但别乱建索引,下面这张表你直接照着判断:
- 等值过滤多:把高选择性等值字段放前面,例如 user_id、tenant_id。
- 需要排序:把 ORDER BY 字段放到复合索引后面,例如 created_at DESC。
- 只查少数字段:考虑覆盖索引,例如 INCLUDE。
- 低选择性字段:单独给 status、gender 这类字段建索引通常收益很低。
覆盖索引示例:
CREATE INDEX CONCURRENTLY idx_orders_cover ON orders (user_id, status, created_at DESC) INCLUDE (amount, order_no);
如果你在找“EXPLAIN ANALYZE教程”,重点看四个指标:实际耗时、实际行数、扫描方式、BUFFERS。估算行数和实际行数差太多,就要检查统计信息。
第3章:验证、维护和最后的真实测速
OK so,索引不是建完就结束。接下来做三件事:更新统计信息、检查膨胀、确认命中率。
ANALYZE orders;SELECT relname, n_live_tup, n_dead_tup FROM pg_stat_user_tables WHERE relname='orders';SELECT relname, idx_scan, idx_tup_read FROM pg_stat_user_indexes WHERE relname='orders';
如果 dead tuple 很高,查询依然抖动,执行:
VACUUM (ANALYZE) orders;
我在本机 8 核 CPU、32GB 内存、PostgreSQL 15、shared_buffers=4GB 的环境里测了 20 次,p95 从 870ms 降到 45ms。测量方式不是靠感觉,而是用应用端日志加 pg_stat_statements 的 mean_exec_time 对照。
怎么验证它真的修好了?照这三步:
- 再次执行
EXPLAIN (ANALYZE, BUFFERS),确认使用目标索引而不是 Seq Scan。 - 连续压测 1-3 分钟,观察接口 p95 是否稳定低于目标值。
- 查看
pg_stat_user_indexes,确认 idx_scan 持续增长,说明线上查询真的命中了索引。
如果你还在搜“PostgreSQL慢查询排查步骤”或“PostgreSQL复合索引最佳实践”,这套流程就够你处理大多数后端接口慢的问题:抓慢 SQL、看执行计划、建匹配索引、ANALYZE、再验证。
最后补一句:免费和官方方案优先用,PostgreSQL 自带日志、EXPLAIN、pg_stat_statements 已经很强;如果你需要稳定访问开发资料或远程协作环境,也可以把 Roxi 作为众多网络工具选项之一了解:wizzegroup.com。觉得这期有用,收藏,评论区丢你的执行计划,我来帮你看!