我曾经天真地以为,数据库慢?加索引啊。
直到有一天,客户的接口超时告警像不要钱似的狂轰滥炸,我手忙脚乱地加了三个索引,结果——响应时间直接翻倍了。
那一刻,我陷入了深深的自我怀疑:索引不是万能的吗?说好的「加索引能提升性能」呢?
后来我花了很长时间才搞明白:索引是把双刃剑,用不好就是给自己挖坑。
先说个冷知识:索引是有成本的
很多人以为索引是免费的,是白给的,是天上掉下来的馅饼。但实际上,每一次 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;
查的字段全在索引里,数据库根本不需要碰主表。
写在最后
索引这个话题,说难其实不难,说简单也不简单。难的是它有很多反直觉的地方,简单的是核心原理就那些。
但最重要的一条原则是:不要为了「好像会用到」而加索引,也不要因为「别人都这么写」就盲目跟随。
先去理解你的数据,理解你的查询,理解数据库的执行计划。然后,让数据说话,而不是让直觉说话。
毕竟,加索引这事,加对了是救火队长,加错了是引火烧身。
祝你的接口永不掉链子。