如何设计数据库性能、慢查询与容量治理
不要先背 EXPLAIN 字段;先解释“为什么只查 50 行,却扫了 280 万行”
这一页只解决一个问题: 数据库设计怎样避免上线后慢到不可用。性能不是独立章节,它会反过来检查你的表、索引、查询、事务和读模型有没有设计错。
Web 记账系统的“本月账单”只展示 50 条记录,但大租户打开页面要 8 秒。CPU 不高,数据库也没宕机。真正的问题是:SQL 返回少,不代表扫描少。
| 现象 | 先别做什么 | 先看什么 |
| 接口慢 | 先加机器 | SQL 指纹、扫描行数、返回行数 |
| CPU 不高 | 以为没压力 | 锁等待、连接等待、IO 等待 |
| 连接池满 | 直接调大连接池 | 连接为什么被长 SQL 或长事务占住 |
| 月底更慢 | 只怪流量 | 报表和导出是否在扫 OLTP 明细 |
| 问题 | 例子 |
| 最高频查询是什么 | 按账本 + 月份倒序看 50 条账目 |
| 排序字段是什么 | occurred_at desc, id desc,不是随便按 created_at |
| 分页会不会深翻 | 避免 offset 深分页,改用游标 |
| 过滤条件是否进入索引 | 账本、状态、时间范围、软删除字段要一起考虑 |
| 写入会被多少索引拖慢 | 每个索引都增加插入、更新、回填和备份成本 |
| 报表是否应该直接打事实表 | 年度统计、导出和大聚合要考虑读模型或异步任务 |
先把最常用列表查询设计对。这个查询如果错,后面再谈容量治理都是补锅。
query:
SELECT id, occurred_at, amount_minor, category_snapshot_id
FROM ledger_entries
WHERE ledger_id = ?
AND status = 'posted'
AND occurred_at >= ?
AND occurred_at < ?
AND deleted_at IS NULL
ORDER BY occurred_at DESC, id DESC
LIMIT 50;
candidate index:
(ledger_id, status, occurred_at DESC, id DESC)
吸收点: 索引不是字段清单,而是查询路径。先确定“谁过滤、谁排序、谁分页”,再谈索引。
| 错误 | 后果 | 改法 |
| 列表页 offset 深分页 | 越翻越慢,还可能重复或漏 | 用游标分页:occurred_at + id |
| 报表实时扫明细 | 月底拖垮在线记账 | 月报读模型、预聚合、异步导出 |
| 事务里做慢计算 | 锁持有时间变长,前台排队 | 事务外计算,事务内只校验和写入 |
| 给每个查询都加索引 | 写入变慢,索引膨胀 | 索引要有用途、收益、成本和退场时间 |
不要把性能当调参: 很多慢不是参数错,而是查询入口、表结构、索引顺序、事务边界和读模型边界没设计。
| 样本 | 通过标准 |
| 普通账本查本月 50 条 | 命中组合索引,扫描行数接近返回行数 |
| 大账本查本月 50 条 | 不因大租户历史数据变成全表扫描 |
| 新增账目写入 | 索引数量没有把写入延迟推高到不可接受 |
| 月底月报生成 | 不直接抢占前台 OLTP 资源 |
| 批量导出 | 进入队列、限并发、可暂停,不长时间占连接 |
| 发布后计划变化 | 能看到 SQL fingerprint、plan hash、扫描行数变化 |
总结: 数据库性能设计先不谈工具。先把高频查询、过滤、排序、分页、索引成本、事务持锁和报表隔离讲清楚。只要“返回 50 行却扫描 280 万行”还解释不了,性能治理就还没开始。