公司某个项目上线后我们发现服务在某些时段会比较慢,打开监控看了下指标,在这个时段:
- CPU: 96%
- IO Wait: 持续偏高
- Disk Read: 从平时 50次/s 涨到 3000次/s
100 并发就把数据库打到这个状态,不太正常。看这几个指标的组合,CPU 和 I/O 同时上涨,磁盘读取量激增,很可能是某个查询在做 Sequential Scan(全表扫描)。某个高频查询没走索引,正在一遍遍翻遍整张表。
找到元凶
打开 GCP Cloud SQL 的 Query Insights,按 Total Load 排序,一条 SQL 占了 80% 的负载:
SELECT
id, employee_id, profile_snapshot, created_at, ...
FROM
employee_snapshot_table
WHERE
created_at >= $1::TIMESTAMP WITHOUT TIME ZONE
ORDER BY
created_at DESC;看起来很普通——按时间筛选、排序,典型的列表页查询。但就是它,正在杀死数据库。
跑一次 EXPLAIN (ANALYZE, BUFFERS) 验尸:
-> Parallel Seq Scan on employee_snapshot_table
Filter: (created_at >= '2024-01-01'::timestamp)
Buffers: shared hit=100 read=2415
-> Sort Method: external merge Disk: 6848kB
Sort Key: created_at DESC三个致命信号:
Parallel Seq Scan— 全表扫描,开了多线程也救不了external merge Disk: 6848kB— 内存放不下,数据被写到硬盘排序(性能杀手)Buffers: read=2415— 单次查询读 19MB 数据,100 并发 = 每秒 1.9GB 磁盘读取
数据库在用硬盘干内存的活儿,能不崩吗?
加索引,起死回生
问题清楚了:created_at 既做筛选又做排序,但没有索引。
生产环境,不能锁表,用 CONCURRENTLY 非阻塞创建:
CREATE INDEX CONCURRENTLY idx_snapshot_created_at
ON employee_snapshot_table (created_at DESC);索引建完,再跑一次压测,世界安静了。
| 指标 | Before | After | 提升 |
|---|---|---|---|
| 查询方式 | 全表扫描 + 硬盘排序 | Index Backward Scan | 算法级优化 |
| 单次耗时 | 177 ms | < 1 ms | 100倍+ |
| 磁盘读取 | 2415 Blocks | 4 Blocks | ↓99.8% |
| CPU 负载 | 96% (多线程) | < 5% (单线程) | 彻底释放 |
Sort 节点消失了——索引本身就是有序的,不用排序。
Seq Scan 变成 Index Scan——不用翻遍全表,直接定位。
为什么会犯这个错?
复盘的时候发现,开发者写代码时关注的是功能能不能跑通,对性能方面的考虑不够仔细。这次故障暴露出我们在数据库设计上缺少统一的思考框架。
于是在 Code Review 环节,我们给团队列了一套规则:凡是涉及数据库的改动,都要对照下面的原则,思考讨论。
默认要加的三类索引
建表时就该考虑:
- Primary Key — 数据库会自动创建
- Unique Key — 像
email、username这类需要快速查找和去重的字段
需要预判的两类场景
-
列表页的排序与筛选
ORDER BY用了哪个字段?考虑加索引(比如这次的created_at)WHERE条件查哪些字段?也要考虑(比如status、type)
-
组合查询 (Composite Index)
- 如果查询总是
WHERE A = 1 AND B = 2这样的组合条件,应该加(A, B)联合索引,而不是分别给 A 和 B 加
- 如果查询总是
避坑指南
不要给低区分度字段加索引
如 gender (男/女)、is_deleted (true/false),加了也没用,只会浪费空间。
善用 Partial Index
如果表中 90% 是逻辑删除的数据,只需给”活着”的数据加索引:
CREATE INDEX idx_active_users ON users(email) WHERE deleted_at IS NULL;写在最后
索引不是让查询变快,而是防止数据库被高并发 I/O 击垮,写 SQL 时,多要问问自己:数据库是在全表扫,还是在查目录(走索引), 另外养成 EXPLAIN 的习惯,防范于未然。