首页 / 后端开发 / PostgreSQL慢查询现场拆解:

PostgreSQL慢查询现场拆解:用EXPLAIN、复合索引和VACUUM把接口从820ms压到37ms

Roxi
Roxi 加速器 — 稳定·快速·安全
全球节点覆盖,支持所有主流平台,一键连接无需配置。新用户免费试用。
立即体验 →

第1章:OK so,先把慢查询抓出来,别凭感觉加索引

性价比88易用性82稳定性95安全性90客服75

兄弟们开机!这里是 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章:接下来建对索引,别把数据库变成“索引垃圾场”

💡STEP 1环境搭建📊STEP 2编码实现🎯STEP 3测试验证📋STEP 4部署上线

很多人搜“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 对照。

怎么验证它真的修好了?照这三步:

  1. 再次执行 EXPLAIN (ANALYZE, BUFFERS),确认使用目标索引而不是 Seq Scan。
  2. 连续压测 1-3 分钟,观察接口 p95 是否稳定低于目标值。
  3. 查看 pg_stat_user_indexes,确认 idx_scan 持续增长,说明线上查询真的命中了索引。

如果你还在搜“PostgreSQL慢查询排查步骤”或“PostgreSQL复合索引最佳实践”,这套流程就够你处理大多数后端接口慢的问题:抓慢 SQL、看执行计划、建匹配索引、ANALYZE、再验证。

最后补一句:免费和官方方案优先用,PostgreSQL 自带日志、EXPLAIN、pg_stat_statements 已经很强;如果你需要稳定访问开发资料或远程协作环境,也可以把 Roxi 作为众多网络工具选项之一了解:wizzegroup.com。觉得这期有用,收藏,评论区丢你的执行计划,我来帮你看!