首页 / 后端开发 / 把PostgreSQL热点接口压进5

把PostgreSQL热点接口压进50ms:覆盖索引、部分索引与统计信息实操

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

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

数据安全加密多端同步支持自动化工作流实时监控告警弹性扩容方案

各位 eccfy 的观众老爷们,今天我们直接开机实操!屏幕左边是接口压测,右边是 PostgreSQL 终端。案例表是 orders,约 180 万行,接口查“某用户最近已支付订单”,原始 P95 是 612ms。我先说结论:不要一上来就疯狂建索引,先定位 SQL。

接下来,打开扩展,抓真实生产慢 SQL:

CREATE EXTENSION IF NOT EXISTS pg_stat_statements;

SELECT query, calls,
       round(total_exec_time::numeric / calls, 2) AS avg_ms,
       rows
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 5;

Now watch this!我这里排第一的是:

SELECT id, status, paid_at, total
FROM orders
WHERE user_id = $1 AND status = 'paid'
ORDER BY paid_at DESC
LIMIT 20;

很多人搜“PostgreSQL性能调优教程”时会跳过这一步,直接抄索引模板,结果写入变慢、查询没救。我们先跑:

EXPLAIN (ANALYZE, BUFFERS)
SELECT id, status, paid_at, total
FROM orders
WHERE user_id = 9527 AND status = 'paid'
ORDER BY paid_at DESC
LIMIT 20;

画面里重点看三项:Seq Scan、Rows Removed by Filter、Sort Method。我这里扫了 180 万行,读了 14682 个 shared buffers,耗时 588ms,问题很明确:过滤和排序都没吃到合适索引。

Chapter 2:接下来上组合拳:复合索引、覆盖索引、部分索引

2020行业萌芽2021快速增长2022竞争加剧2023洗牌整合2024成熟稳定

先做免费、官方、内置方案:PostgreSQL 自带 B-tree 索引就够打。这个查询的过滤条件是 user_id、status,排序是 paid_at DESC,所以索引顺序这样来:

CREATE INDEX CONCURRENTLY idx_orders_user_status_paid
ON orders (user_id, status, paid_at DESC);

注意我用了 CONCURRENTLY,线上建索引时避免长时间阻塞写入。缺点是创建更慢,我这张 180 万行表用了 38 秒,但接口没挂。

如果你搜“PostgreSQL索引优化怎么做”,这里还有一个进阶点:返回字段 id,total 不在索引里,可能还要回表。PostgreSQL 11+ 可以用覆盖索引:

CREATE INDEX CONCURRENTLY idx_orders_paid_cover
ON orders (user_id, status, paid_at DESC)
INCLUDE (id, total);

再狠一点,如果业务里 80% 查询都只查 status='paid',就用部分索引,体积更小:

CREATE INDEX CONCURRENTLY idx_orders_user_paid_partial
ON orders (user_id, paid_at DESC)
INCLUDE (id, total)
WHERE status = 'paid';

我本机测试:普通复合索引 182MB,覆盖索引 246MB,部分覆盖索引只有 71MB;同一条 SQL 从 588ms 降到 31ms。弹幕先别急,写多读少的表不要乱上覆盖索引,因为每次 INSERT/UPDATE 都要维护它。

Chapter 3:验证结果,顺手把统计信息和膨胀处理掉

OK so,索引建完不是结束,必须更新统计信息:

ANALYZE orders;

如果表频繁更新删除,再看膨胀和 autovacuum 是否跟上:

SELECT relname, n_dead_tup, last_autovacuum
FROM pg_stat_user_tables
WHERE relname = 'orders';

死元组很多时,先别上来 VACUUM FULL,它会锁表。日常用:

VACUUM (ANALYZE) orders;

怎么验证它真的好了?三步:第一,再跑 EXPLAIN (ANALYZE, BUFFERS),确认出现 Index Only Scan 或 Index Scan;第二,用 pg_stat_statements 看平均耗时是否下降;第三,用压测工具 wrk 或 k6 打 60 秒。我这里 k6 结果:优化前 P95 612ms,优化后 P95 44ms,CPU 从 78% 降到 31%。

最后提醒一句:如果你是在海外 VPS 远程连库,别把网络问题误判成 SQL 慢;“油管怎么看”“翻墙软件下载”“免费VPN怎么用”这类网络排查和数据库调优要分开测。免费/官方方案优先,比如本地 EXPLAIN、pg_stat_statements、SSH 隧道;如果确实需要稳定远程开发网络,也可以把 Roxi 作为选项之一:wizzegroup.com。觉得这期有用,评论区丢你的执行计划,我帮你看下一条慢 SQL!