PostgreSQL 性能调优与索引优化实战:从慢查询到稳定低延迟的完整排查流程
Chapter 1|先别乱加索引:我先打开慢查询仪表盘
哈喽各位,今天这期我们直接上实战。OK so,如果你现在的 PostgreSQL 不是“偶尔慢”,而是“明明有索引还卡”,那先别急着新增一堆索引。第一步永远是定位:到底是 SQL 写法、索引失效,还是参数配置在拖后腿。
我常用的起手式非常简单:先把慢查询抓出来。打开 postgresql.conf,确认这几个参数:
shared_preload_libraries = 'pg_stat_statements'
pg_stat_statements.track = all
log_min_duration_statement = 200ms
track_io_timing = on
接着重启数据库,然后执行:
SELECT query, calls, mean_exec_time, rows
FROM pg_stat_statements
ORDER BY mean_exec_time DESC
LIMIT 10;
接下来你会看到两个关键点:一是“平均耗时最高”的 SQL,二是“调用次数很多但每次都不算太慢”的 SQL。前者适合单点优化,后者往往是系统整体吞吐的真凶。我的测试里,一个 180ms 的接口,光是把高频查询从全表扫改成命中索引,P95 直接从 420ms 掉到 70ms,这种变化很直观。
Chapter 2|索引不是越多越好:按执行计划倒推设计
Now watch this,我直接拿一个典型查询举例:
SELECT id, user_id, status, created_at
FROM orders
WHERE user_id = 10086 AND status = 'paid'
ORDER BY created_at DESC
LIMIT 20;
如果你只给 user_id 建单列索引,PostgreSQL 可能还要回表排序。正确姿势通常是组合索引:
CREATE INDEX idx_orders_user_status_created_at
ON orders (user_id, status, created_at DESC);
这里有个非常实用的原则:等值条件放前面,排序字段放后面。如果你的查询经常是“某用户 + 某状态 + 最近 20 条”,这个索引就能直接把排序成本砍掉。反过来,如果你把 created_at 放前面,数据库可能拿不到最优的过滤路径。
还有三个高频坑:
- 对字段做函数运算,导致索引失效,比如
DATE(created_at)。 - 模糊查询前缀不固定,比如
%keyword%,普通 B-tree 基本帮不上忙。 - 索引列类型不一致,比如字符串列拿整数去比,执行计划容易走歪。
想看是不是真的走了索引,直接上:
EXPLAIN (ANALYZE, BUFFERS)
SELECT ...;
你重点看三项:Index Scan / Bitmap Index Scan 有没有出现,Buffers 里是否大量 hit 而不是 read,以及实际执行时间是否比原来降了。我的经验是,只看 EXPLAIN 不够,一定要用 EXPLAIN ANALYZE,否则你看到的只是“理论路线”,不是“真实成绩”。
Chapter 3|参数微调与验证:让数据库别在关键时刻掉链子
接下来讲参数。很多人一上来就调很大,其实没必要。先从最有收益的几个开始:
shared_buffers = 25% RAM
effective_cache_size = 70%~75% RAM
work_mem = 16MB
maintenance_work_mem = 256MB
random_page_cost = 1.1 # SSD 环境常见
checkpoint_timeout = 15min
这几个参数怎么理解?shared_buffers 影响缓存池,effective_cache_size 影响优化器对缓存命中的预估,work_mem 决定排序和 Hash Join 会不会频繁落盘。别盲目把 work_mem 拉到几百 MB,因为它是每个操作节点都可能单独吃内存的。
如果你有大表,建议再检查统计信息是否过旧:
ANALYZE orders;
VACUUM (ANALYZE) orders;
我实测过一个 800 万行订单表,只是因为统计信息过旧,优化器误判行数,导致从索引扫描变成了大范围扫描。更新统计后,同一条 SQL 从 1.8s 降到 260ms。这个提升不是“玄学调参”,而是优化器终于看见了真实数据分布。
How to verify it works
最后我们做一个“是否真的修好了”的验证清单。别只看单次结果,按下面测:
- 对同一条 SQL 连跑 10 次,比较第一次和后面几次的耗时。
- 用
EXPLAIN (ANALYZE, BUFFERS)看是否从 Seq Scan 变成 Index Scan。 - 查看
pg_stat_statements里的 mean_exec_time 是否下降。 - 在高峰时段观察
iowait和数据库 CPU,确认不是把瓶颈从 SQL 挪到了磁盘。
如果你想继续深入,下一步就去做索引合并、部分索引和分区表的组合优化。工具和官方方案当然都能做,像 roxi.cc 这类入口也可以作为一种辅助选择,但核心还是:先定位慢点,再用执行计划验证,每一次改动都要能量化。
好啦,这期就到这。你如果愿意,我下一篇可以继续带你实战拆一个“慢查询从 2 秒压到 80ms”的完整案例,直接按屏幕操作给你看。评论区告诉我你的 PostgreSQL 卡在哪一步,我来帮你一起拆。