一个缺失索引的列是如何搞垮我们的数据库的
一个缺失的索引将 8 毫秒的 API 变成了 8 秒的噩梦。
在一次重大发布前,我们进行了压力测试。在只有 100 条记录的本地开发环境中运行良好。随后,我们模拟了 500 名用户,针对生产规模的 150 万行数据进行测试。
结果非常糟糕:
- API 响应时间从 45 毫秒飙升至 8,000 毫秒。
- 数据库 CPU 使用率达到 100%。
- 连接池耗尽,导致超时错误。
我们检查了慢查询日志,发现一个根据用户 ID 和状态获取订单历史记录的接口。
在 PostgreSQL 上运行 EXPLAIN ANALYZE 显示为 Sequential Scan(全表扫描)。由于 user_id 上没有索引,引擎会对每个请求读取全部 150 万行数据。
在 100 个并发请求下,它一次性扫描了 1.5 亿行数据。
修复只用了五分钟。
我们没有只在 user_id 上创建简单索引,而是创建了一个复合索引 (user_id, status, created_at DESC)。这让数据库能够:
- 按
user_id过滤。 - 按
status过滤。 - 立即返回最新的行。
- 跳过额外的排序步骤。
我们使用了 CREATE INDEX CONCURRENTLY,以便在操作期间保持表处于非锁定状态。
修复之后:
- 查询时间从 8,150 毫秒降至 0.14 毫秒。
- API 延迟从 8 秒降至 12 毫秒。
- CPU 使用率从 100% 降至 8% 以下。
教训:
- 本地测试具有误导性;100 行数据无法代表百万级数据。
- 为外键建立索引——大多数 ORM 都会忽略这一点。
- 在购买更多硬件之前,先运行
EXPLAIN ANALYZE。 - 根据你实际运行的查询来设计索引。
不要先扩容服务器。先优化查询。
