为什么你的数据库索引越加越慢?来自一线踩坑的深度剖析

2026-05-22 14 0

我曾经天真地以为,数据库慢?加索引啊。

直到有一天,客户的接口超时告警像不要钱似的狂轰滥炸,我手忙脚乱地加了三个索引,结果——响应时间直接翻倍了。

那一刻,我陷入了深深的自我怀疑:索引不是万能的吗?说好的「加索引能提升性能」呢?

后来我花了很长时间才搞明白:索引是把双刃剑,用不好就是给自己挖坑。

先说个冷知识:索引是有成本的

很多人以为索引是免费的,是白给的,是天上掉下来的馅饼。但实际上,每一次 INSERT、UPDATE、DELETE 操作,数据库除了修改数据本身,还要顺手更新索引。

一个表上索引越多,写入性能就越差。这不是玄学,这是B+树的数学:每次数据变更,数据库要把索引路径上的所有节点都改一遍。索引数量级越大,需要维护的B+树就越多。

我见过一个生产环境的表,12个字段,16个索引。开发者的理由是「怕以后用到」。结果是每次插入一条记录,数据库要默默干16次索引维护。这哪是数据库,这是冤大头。

你的索引为什么形同虚设?

最常见的原因:你的 WHERE 条件根本没用上索引。

看看下面这个查询:

SELECT * FROM orders WHERE YEAR(created_at) = 2024 AND status = paid;

你觉得在 created_at 上加个索引就够了?太天真了。

因为 YEAR() 函数把索引字段包了一层,数据库引擎拿到的是 YEAR(created_at) 的结果,而不是 created_at 本身。这意味着索引列参与计算会让索引失效,MySQL 只能老老实实做全表扫描。

正确姿势:

SELECT * FROM orders WHERE created_at >= 2024-01-01 AND created_at < 2025-01-01 AND status = paid;

不要在索引列上套函数,这是最基本的素养。

联合索引的坑,比你想象的深

假设有个表:users(id, name, email, status, created_at)

你兴冲冲地建了个索引:

ALTER TABLE users ADD INDEX idx_common(name, email, status);

然后你的查询是:

SELECT * FROM users WHERE status = active AND email = test@example.com;

恭喜你,这个索引完全没用

因为联合索引遵循最左前缀原则。你定义了(name, email, status),但查询跳过了 name 和 email,直接查 status。这就像你有一本按「省份-城市-区县」排序的电话簿,却直接翻到「所有叫张三的人」那页——排序白做了。

那如果我经常按 status 查询呢?再建一个单独的索引?没错,就是这么反直觉:

ALTER TABLE users ADD INDEX idx_status(status);

索引不是越多越好,而是越精准越好。

数据量小时,全表扫描反而更快

这是很多人容易踩的另一个坑:执着于「必须有索引」。

当表的数据量只有几百上千行时,数据库优化器会做出一个违背祖宗的决定:不用索引,直接全表扫描。为什么?因为索引本身也需要随机IO,在小数据量场景下,全表扫描的顺序IO反而更快。

所以你会看到奇怪的现象:开发环境跑得飞快,生产环境慢成狗。不是数据库抽风,是数据量变了,优化策略也跟着变了。

说个真实的血泪史

之前接手一个老项目,有个接口巨慢,跑了 8 秒。用户已经习惯性抱怨了,开发者也习以为常了,觉得「数据量大,正常的」。

我一看,WHERE 条件里有个隐式类型转换:

SELECT * FROM products WHERE product_id = 12345;

product_id 是 bigint 类型,但传进来的是字符串。MySQL 会把数字转成字符串比较,索引?不好意思,用不了。

改成一刀:

SELECT * FROM products WHERE product_id = 12345;

去掉引号。接口响应时间从 8 秒变成 80 毫秒。

没有加索引,没有分库分表,没有缓存,就改了一个引号。

你说气不气人?

怎么判断索引到底有没有生效?

用 EXPLAIN。这是数据库给你的debug信息,比什么性能监控都准。

EXPLAIN SELECT * FROM orders WHERE status = pending;

看输出的 type 列:

  • ALL:全表扫描,凉了
  • index:扫了整个索引树,还行但不够好
  • range:范围查询,最好是这种
  • ref:索引被使用了,稳了
  • const:常量查询,完美

另外注意 rows 列,它告诉你数据库预估要扫描多少行。如果你的表有 100 万行,但 EXPLAIN 显示要扫 80 万行,那这个索引基本等于摆设。

给务实派的几条建议

说了这么多,不是让你恐惧索引,而是让你敬畏索引。几个实操建议:

第一,建索引前先看查询模式。你的系统读多还是写多?最频繁的查询是什么?先回答这两个问题,再决定索引怎么建。

第二,定期清理无用索引。可以通过查询 sys.schema_unused_indexes 或者 performance_schema 找到长期没被用过的索引。留着它们只有坏处没有好处。

第三,区分主键索引和普通索引。主键索引是聚簇的,数据就挂在索引上;普通索引是辅助的,查到主键还得回表。理解这个,你就能算清楚每次查询到底要几次IO。

第四,不要迷信最左前缀。如果你的查询真的需要多个字段,试试看能否用覆盖索引——把要查的字段直接放进索引里,省去回表。

-- 覆盖索引示例
ALTER TABLE orders ADD INDEX idx_covered(status, created_at, amount);

SELECT status, created_at, amount FROM orders WHERE status = paid;

查的字段全在索引里,数据库根本不需要碰主表。

写在最后

索引这个话题,说难其实不难,说简单也不简单。难的是它有很多反直觉的地方,简单的是核心原理就那些。

但最重要的一条原则是:不要为了「好像会用到」而加索引,也不要因为「别人都这么写」就盲目跟随。

先去理解你的数据,理解你的查询,理解数据库的执行计划。然后,让数据说话,而不是让直觉说话。

毕竟,加索引这事,加对了是救火队长,加错了是引火烧身。

祝你的接口永不掉链子。

相关文章

健身卡办了一年了,我可能去过的次数还没我丢过的钥匙多
为什么你的数据库索引越加越慢?来自一线踩坑的深度剖析
表情包斗图:我为什么永远输给别人?
🦞 我与 OpenClaw 的相爱相杀:一只小龙虾的AI驯化记录
拆快递一时爽,退货流程火葬场:我的网购退货血泪史
当小龙虾遇上AI:那些让我惊呼”卧槽”的新奇玩意儿

发布评论