半夜报警?这3个索引“负优化”案例让我长记性了

不知道大家有没有这种经历:明明给表加了索引,查询速度不仅没起飞,数据库的CPU反而先爆表了,甚至连写入都开始阻塞。

我以前总觉得,“慢查询”最好的解药就是加索引。只要业务方喊慢,我就去 ADD INDEX。直到三年前那次双11大促前的压测,我亲手搞挂了一个核心服务的数据库,才彻底明白:索引这把双刃剑,很多时候是拿反了的。

今天咱们不聊那些枯燥的B+树原理,我想复盘这几年在一线实战中踩过的3个典型“坑”。这些都是真金白银砸出来的教训,希望能帮大家避开半夜两点的报警电话。

一、 “为了快,我把所有字段都加了索引”

配图

这大概是很多新手(包括当年的我)最容易犯的错。

【真实场景】 当时我们在做一个用户行为日志表,数据量增长很快,每天大概新增500万条。业务方需求很多,今天想按 user_id 查,明天想按 event_type 查,后天又要按 device_id 统计。

为了省事,也为了“未雨绸缪”,我一股脑给这几个高频字段全加了单列索引。心想:这下总没问题了吧?

【踩坑结果】 上线运行了一周,问题来了。虽然查询没问题,但写入性能(Insert TPS)直线下降。原本每秒能抗3000次写入,现在到了1500次就开始报错超时。DBA直接把监控图甩我脸上:磁盘IO几乎被打满,Buffer Pool全是脏页。

【复盘与改进】 这就是典型的“索引过度”。每一条 INSERT 语句,数据库不仅要写主键数据,还要维护那5、6棵额外的B+树。对于写多读少的日志型业务,这简直是灾难。

我后来做了两件事来救火:

  1. 砍掉单列索引:只保留了 user_id,其他非核心字段的索引全部删掉。
  2. 建立联合索引:根据最长用的查询模式,建了一个 (user_id, event_time) 的联合索引,覆盖了80%的查询场景。

【避坑指南】

如果你的表是“写多读少”(比如日志、流水表),单表索引数量建议控制在3个以内。别为了那1%的查询需求,牺牲99%的写入性能。

二、 联合索引的“顺序”陷阱:并不只是为了好看

既然单列索引不好用,那我就用联合索引呗?这里面的坑更隐蔽。

【真实场景】 有个订单表 orders,业务代码里有一个最高频的查询:查找某个店铺、在某个时间段内、特定状态的订单。

代码大概是这样的:

SELECT * FROM orders 
WHERE shop_id = 10086 
AND create_time > '2023-10-01 00:00:00' 
AND status = 'PAID';

配图

我当时自信满满地建了个索引:idx_shop_time_status (shop_id, create_time, status)。 我觉得这三个字段都覆盖了,肯定也是完美的。

【踩坑结果】 查询确实比没索引快,但依然有几百毫秒的延迟。用 EXPLAIN 一看,key_len 的长度不对,只有前两个字段生效了,status 字段压根没用上索引!

【复盘与改进】 这里触碰了MySQL联合索引的一个硬核规则:范围查询(Range)会阻断最左前缀的匹配

在我的索引 (shop_id, create_time, status) 中:

  1. shop_id 是等值匹配,没问题。
  2. create_time 是范围查询(>),索引在这里虽然生效了,但索引的匹配就到此为止了
  3. 后面的 status 字段,数据库只能回表后一个个过滤,或者在索引树上硬扫,无法利用索引的快速定位能力。

【修正方案】 调整索引顺序,把涉及范围查询的字段放到最后: 修改为:idx_shop_status_time (shop_id, status, create_time)

再次执行 EXPLAIN,类型从 range 变成了 ref(或者更高效的利用),查询时间直接降到了几十毫秒。

【避坑指南】 记住这个口诀:“等值在前,范围在后”。在设计联合索引时,把 WHERE 条件里用 = 的字段尽量往左边放,用 ><BETWEEN 的字段往右边放。

三、 隐式转换:代码里一个微小的疏忽,搞挂了数据库

这个坑是最冤的,因为它看起来完全不是数据库的问题,而是代码写得不严谨。

【真实场景】 这是一个用户搜索接口,根据手机号查找用户信息。 表结构:

CREATE TABLE users (
  id int PRIMARY KEY,
  phone varchar(20), -- 注意这里是 varchar
  name varchar(50),
  KEY idx_phone (phone)
);

索引有了,SQL看起来也很简单:SELECT * FROM users WHERE phone = 13800138000;

某天下午,运营做活动群发短信,用户蜂拥而至查状态,数据库CPU瞬间飙到100%,整个应用直接卡死。

【踩坑结果】 排查慢日志发现,这条简单的SQL扫描行数(Rows_examined)竟然是全表行数! 原因在于:开发人员在Java代码里,把手机号当作 Long 类型传给了数据库,而数据库里存的是 Varchar

【复盘与改进】 当字段类型是字符串,而查询条件是数字时,MySQL会做隐式类型转换。它会把表中每一行的 phone 字段转换成数字,再去和输入值比较。 这就相当于每一行都执行了一次函数操作:CAST(phone AS signed)在索引字段上做函数操作,索引直接失效,退化为全表扫描。

配图

【修正方案】 不需要改数据库,只需要改代码(或者SQL):

-- 加上引号,告诉MySQL这是个字符串
SELECT * FROM users WHERE phone = '13800138000'; 

改完上线,CPU立马降到了5%以下。从那以后,我在Code Review时,对涉及数据库字段类型的代码看得格外仔细。

总结与落地

数据库索引优化,真不是背几个面试题就能搞定的。它需要你对业务场景有极深的理解,以及对执行计划(Explain)的敬畏。

如果你想立刻改善手头项目的数据库性能,我有3个建议,明天上班就能做:

  1. 开启慢查询日志:设置阈值(比如1秒),跑一天,把抓到的Top 10慢SQL拉出来分析。这是最直接的线索。
  2. 检查“隐式转换”:搜索代码库,看看有没有用数字去查字符串字段的情况,尤其是在老旧系统中,这非常常见。
  3. 清理无效索引:用 sys.schema_unused_indexes (MySQL 5.7+) 看看哪些索引从来没被用过。它们在默默吃你的磁盘空间和写入性能,删了它们就是做减法优化。

最后想问问大家: 你在生产环境中遇到过最离谱的“慢查询”是什么原因造成的?是没建索引,还是更奇葩的理由?欢迎在评论区分享你的“踩坑”故事,咱们一起避雷。