我们排查过的性能问题里,最终落在数据库上的占大多数——不是数据库不够快,而是索引、SQL 写法和连接配置出了问题。这篇按"先看什么、再改什么"的顺序整理,末尾附一份可以直接用的自检清单。
先量再改:慢查询日志 + EXPLAIN
优化最忌讳凭感觉加索引。我们的固定动作是两步:先开慢查询日志(slow_query_log = ON、long_query_time = 1),让它跑一天,看哪些 SQL 真的慢、慢多少次;再用 EXPLAIN 看执行计划,确认慢在哪一步。
🔍 EXPLAIN 看三个字段就够:type 列避免 ALL(全表扫描)与 index(全索引扫描),目标是 ref / eq_ref / range;rows 是预估扫描行数,越小越好;Extra 里出现 Using filesort 或 Using temporary,说明排序或分组没吃到索引。
三种最常见的"索引失效"
很多慢查询并不是没索引,而是索引没用上。这三类我们在项目里反复遇到:
- 隐式类型转换。字段是
varchar,查询写成WHERE order_no = 12345(传了数字),索引直接失效。参数化查询时注意类型对齐。 - 在索引列上做运算或函数。
WHERE DATE(created_at) = '2026-01-01'用不上索引,改成范围条件created_at >= '2026-01-01' AND created_at < '2026-01-02'。 - 联合索引不满足最左前缀。
(a, b, c)能覆盖WHERE a=1与WHERE a=1 AND b=2,但覆盖不了WHERE b=2。排序与分组也要和索引列顺序一致,否则照样 filesort。
顺带一句:索引不是越多越好。每个索引都要在写入时维护,还会占空间;我们更倾向"先删掉没被用到的冗余索引,再补真正需要的"。
一个真实的优化过程
某订单列表查询耗时 5.8 秒。EXPLAIN 显示:全表扫描约 200 万行 + 文件排序 + 临时表。三步处理:
- 建立
(user_id, created_at)联合索引 → type 变为 range,预估行数降到几百。 - 分页只取需要的一页(
LIMIT 20),避免一次把全部数据捞回应用层。 - 把查询字段收敛成覆盖索引能提供的列 → Extra 变成 Using index,去掉回表。
优化后 5.8s → 80ms。真正起作用的其实是前两步,覆盖索引是锦上添花。
分页与连接池:两个容易被忽略的地方
深度分页。LIMIT 100000, 20 会让数据库扫描并丢弃前十万行——页数越深越慢。业务上能接受时改用游标分页(带上上一页最后一条的 id 或时间),WHERE id > ? ORDER BY id LIMIT 20,成本恒定。
连接池。池子太小会在高并发时排队,太大反而拖垮数据库(大量线程上下文切换)。我们的做法是从一个保守值开始(例如 CPU 核数 × 2 + 磁盘数 作为起点),再按压测调整,并确认应用侧用完即还、没有泄漏。
📋 数据库自检清单
- □ 慢查询日志开着吗?阈值是多少?(建议先设 1 秒)
- □ 高频 SQL 是否都用 EXPLAIN 看过?type 里有没有 ALL?
- □ 有没有在索引列上做函数运算或类型不一致的比较?
- □ 是否存在深度分页?能否改成游标分页?
- □ 有没有长期没被命中的冗余索引?
- □ 连接池大小是否压测过?应用侧有没有连接泄漏?
- □ 大表有没有归档或分区策略(历史数据不必和热数据挤在一起)?
比调优更重要的是可观测
真正让这类问题不再"突然爆发"的,是把监控补齐:慢查询趋势、连接池使用率、关键 SQL 的 P95 耗时。有了这些,性能问题会在变成故障前先被看到。我们在项目里默认把这几项纳入上线检查,如果你手上有系统正在"高峰期特别慢",欢迎联系我们,先把慢查询日志跑起来,往往第一天就能定位到主因。
