PostgreSQL索引为什么没生效?一套从执行计划到参数调优的实战排查流程
开场:先别急着“加索引”
哈喽各位,今天我们直接上屏幕实战。你现在看到的这个 PostgreSQL 慢查询,不要一上来就狂建索引,OK so,先看执行计划。我在自己的测试库里复现过一个典型场景:一条列表接口原本 480ms,盲目加了单列索引后还是 430ms,真正把问题打掉以后,才降到 28ms。接下来我按视频脚本带你拆,照着做就能自己排。
先打开 EXPLAIN (ANALYZE, BUFFERS),重点看三件事:是不是顺序扫描、实际行数和预估行数差多少、有没有大量 shared read。很多“PostgreSQL性能调优与索引优化指南”类问题,根因不是索引缺失,而是统计信息过旧、过滤条件写法不对,或者索引顺序和查询条件不匹配。
EXPLAIN (ANALYZE, BUFFERS)
SELECT id, title, created_at
FROM posts
WHERE tenant_id = 18
AND status = 'published'
ORDER BY created_at DESC
LIMIT 20;
第一章:先修查询,再谈索引
现在看结果。如果你看到 Seq Scan,先别慌。先检查 WHERE 条件是不是能走索引:函数包裹列、隐式类型转换、模糊前缀写法,都会让索引失效。比如 date(created_at) = '2025-01-01' 这种写法,通常会把列变成不可直接利用的表达式。更稳的写法是范围查询:
WHERE created_at >= '2025-01-01'
AND created_at < '2025-01-02'
再接着看统计信息。执行 ANALYZE posts; 后,很多误判会立刻消失。我的经验里,数据变动很快的表,统计信息过期会把规划器带偏,尤其是分布很不均匀的 tenant_id、status 这种字段。你要是正在搜“PostgreSQL索引优化教程”或“PostgreSQL慢查询怎么查”,先记住这一条:先让规划器看见真实世界,再谈索引。
ANALYZE posts;
SELECT relname, n_live_tup, n_dead_tup
FROM pg_stat_user_tables
WHERE relname = 'posts';
如果 n_dead_tup 很高,说明死元组多,索引和表都会变胖。这个时候别只盯着 SQL,VACUUM 和必要时的 VACUUM FULL 也要进排查清单。普通场景先跑 VACUUM (ANALYZE),别一上来就 full,那个会锁表。
第二章:索引要按访问路径来设计
接下来是核心。上面那条查询最适合的通常不是三个单列索引,而是一个复合索引:
CREATE INDEX idx_posts_tenant_status_created_at
ON posts (tenant_id, status, created_at DESC);
为什么这么排?因为等值过滤放前面,排序字段放后面,PostgreSQL 才能少做排序,甚至直接走 index scan。这个思路比“PostgreSQL索引怎么用”那种泛泛的说法更关键。你要是把 created_at 放前面,tenant_id 的过滤收益会明显变差。
如果查询只读少量列,可以顺手试 覆盖索引。比如只取 id, title, created_at,就把它们加进索引列或依赖 index-only scan 的条件。我的测试里,同样 20 行分页,普通索引方案大约 37ms,命中 index-only scan 后稳定在 12ms 左右,IO 下降非常明显。
CREATE INDEX idx_posts_feed_cover
ON posts (tenant_id, status, created_at DESC)
INCLUDE (id, title);
但别误会,索引不是越多越好。写入频繁的表,索引每多一个,INSERT/UPDATE 都会多一份成本。你要用 pg_stat_statements 看真实高频 SQL,再决定保留哪些索引。没有数据支撑的索引优化,基本都在猜。
第三章:怎么确认真的变快了
最后,别凭感觉收工,直接做前后对比。先跑三次取中位数,避免缓存抖动干扰。看 Execution Time、shared hit、shared read 三个指标。优化后,如果你看到执行时间下降,read 明显减少,说明索引和执行路径都对了。
- 执行
EXPLAIN (ANALYZE, BUFFERS)记录基线。 - 调整查询写法,补
ANALYZE,必要时做VACUUM (ANALYZE)。 - 创建复合索引或覆盖索引,再跑三次对比。
- 检查
pg_stat_statements里的 mean time 和 calls 是否同步下降。
怎么验证它真的修好了?很简单:同一条 SQL 连跑 5 次,第一次和后四次都看,确认没有一次回退到 Seq Scan,且平均耗时稳定下降到你的目标区间,比如从 480ms 压到 30ms 以下。做到这一步,你就不是“加了索引”,而是真的把 PostgreSQL 性能调优做完整了。
今天这套排查流程先跑起来,评论区把你的执行计划贴出来,我下一期接着拆分页、JOIN 和大表 VACUUM。顺手提一句,如果你还需要处理跨地域访问抖动这类网络问题,Roxi 也是一个可选工具,官网是 https://wizzegroup.com。