你的数据库为什么慢得像蜗牛?

页面卡在 loading 转圈,用户刷着刷着就关掉了,服务器告警邮件一晚上收三封……
这哪是查数据,简直是等审批。

你真的看懂执行计划了吗?

别急着加索引、改 SQL,先让数据库自己说说它打算怎么干。
MySQL 里加个 EXPLAIN 就行——就放在你那条慢查询前面,回车一跑,它就把“心里话”全吐给你看了。

重点盯三个地方:

  • type 列:值是 ALL?完蛋,它正准备扫完整张表;要是 refeq_ref,说明至少摸到了索引的边。
  • rows 列:数字后面带个“万”字?那就不是“查得慢”,是“查得太多”。
  • Extra 列:看见 Using filesortUsing temporary?别犹豫,这就是性能雪球滚起来的第一铲雪。

一个真实翻车现场:某次后台列表页打开要十几秒。EXPLAIN 一贴,type=ALLrows=120万+Extra=Using filesort; Using temporary——全齐了。查下去发现,ORDER BY created_atGROUP BY status 两个操作,字段上一个索引都没有。数据库只能硬扛。

索引建了,为什么还是慢?

建了索引 ≠ 查询变快。我们踩过的坑,基本都卡在这几个地方:

  • 索引被“绕开”了:比如 WHERE DATE(create_time) = '2024-01-01',函数一裹,索引直接失效。
  • 索引建在了“没用的字段”上:给 is_deleted(只有 0/1)单独建索引?优化器看了都想笑。
  • 索引堆太多:每多一个索引,INSERT/UPDATE 就多一份维护成本。有些表索引比字段还多,写操作早就在偷偷骂娘了。

一个典型错位:用户表有个 (city, age) 联合索引。查 WHERE city='上海' ORDER BY age,跑得飞快;但换成 WHERE age > 25,索引就彻底歇菜——因为不满足最左前缀原则。索引不是摆设,得按你真正常写的 WHERE + ORDER BY 组合来设计。

除了加索引,还能做什么?

索引救不了所有问题。当单表数据涨到几千万,或者报表类查询动不动 JOIN 五张表时,就得换思路了。

  • 读写分离:主库专心写,从库分摊读。你用的是 MySQL?主从配置早就内置好了,连中间件都不用装。
  • 分库分表:不是一步到位,而是当 EXPLAIN 显示 rows 总是百万起步、且索引已尽力时,才该认真考虑。
  • SQL 本身能省则省
    • 别写 SELECT *,前端只要头像和昵称,你就别把整张用户表拖回来;
    • JOIN 前先确认关联字段有没有索引;
    • 循环里一条条 INSERT?换成 INSERT INTO ... VALUES (...), (...), (...) 批量插。

我们亲手调过的一个报表:原来关联五张表,实时 COUNT、SUM、GROUP,跑一分钟。后来:

  1. SELECT * 缩成 7 个必要字段;
  2. 补全所有 ON 条件字段的索引;
  3. 把一个子查询改成 LEFT JOIN
  4. 把部分聚合结果挪到凌晨定时任务里算好存进汇总表。
    最后效果?明显提升。

慢查询日志是你的宝藏

它不是摆设,是你不用花钱就能拿到的“数据库体检报告”。
MySQL 默认关着,你要手动打开:进 my.cnf,加上

slow_query_log = ON  
long_query_time = 2  
slow_query_log_file = /var/log/mysql/slow.log

重启服务,它就开始记账了。

分析也不用复杂工具:

  • mysqldumpslow -s t /var/log/mysql/slow.log:按总耗时排序,一眼揪出最耗命的几条;
  • 更推荐 pt-query-digest(Percona Toolkit 里的),一行命令就能输出“谁最慢、慢在哪、锁了多久”。

一次意外收获:有天翻慢日志,发现某条统计 SQL 每周一凌晨特别慢。顺藤摸瓜,发现归档脚本和它同时锁了同一张订单表。把归档任务往后推两小时,慢查询直接消失。这种问题,不看日志,永远在盲人摸象。

应用层设计如何给数据库减负?

很多慢,真不怪数据库——是应用把它当成了万能胶水。

  • 缓存不是可选项,是必选项:用户资料、城市列表、开关配置……这些读多写少的数据,Redis 里放一份,接口响应快一倍,数据库也喘口气。你项目里已经在用 Redis?那就从最常查的那张字典表开始缓存。
  • 别让数据库干应用的事:那个要 JOIN 六张表、返回 32 个字段的“巨无霸查询”,前端真需要一次性全拿?拆成用户信息 + 订单摘要 + 地址列表三个小请求,体验反而更稳。
  • 高频计数别硬 COUNT:比如文章阅读数、商品销量,每次展示都 SELECT COUNT(*) FROM ... WHERE xxx?高峰期直接拖垮。改成:写操作发生时,同步 INCR Redis key;展示时直接 GET;再用简单定时任务,每天凌晨把 Redis 数据刷回数据库做备份。

一个落地例子:某社区帖子点赞数,原来每刷新一次就 COUNT 一遍。优化后:点赞动作触发 INCR post:123:likes;前端 GET post:123:likes;凌晨脚本把 Redis 里的值写回 posts.likes 字段。数据库压力少了 70% 以上,而且点赞响应快到感觉不到延迟。

今天下班前就能做的一件事

打开你正在用的生产环境数据库(就是你每天 mysql -u root -p 连的那个),执行这三步:

  1. 运行 SET GLOBAL slow_query_log = ON; SET GLOBAL long_query_time = 2;(临时开启,不影响重启);
  2. 让它跑满今天剩下的时间;
  3. 明早第一件事:用 mysqldumpslow -t 10 /var/log/mysql/slow.log(路径按你实际日志位置填)拉出最慢的 10 条;
  4. 对排名第一的 SQL,粘贴到数据库里,在前面加上 EXPLAIN,回车运行;
  5. type 是不是 ALLrows 是不是高得离谱,Extra 有没有刺眼的红字。

然后——就地加个索引,或删掉一个多余的 ORDER BY,或把 WHERE DATE(xxx) 改成 WHERE xxx BETWEEN ... AND ...
改完再跑一遍 EXPLAIN,看到 rows 数字小了一半,你就知道:这事,真能成。