早啊,各位。今天不讲概念,直接上硬菜。
我做过后端开发,也帮人做过数据库优化,见过最多的场景就是:SQL 写得挺漂亮,但跑了十几秒出不来结果,工程师急得抓头发。排查一圈,最后发现——没加索引,或者索引加错了。
索引这个事,说简单也简单,说坑深也是真的坑。今天就把我踩过的坑、总结的经验,一次性倒出来。
先说原理,别急着动手
很多人知道索引能提速,但不知道为什么。我简单说一下:
没有索引的时候,MySQL 查数据是一行一行扫的,叫全表扫描(Full Table Scan)。数据少还好,数据上了百万、千万,你试试看。
索引的本质是一棵 B+ 树。MySQL InnoDB 引擎里,每个索引都对应一棵 B+ 树,主键索引的叶子节点存整行数据,普通索引的叶子节点存主键值,查到之后再回表拿完整数据。
所以:索引不是越多越好。每加一个索引,就多一棵 B+ 树,写操作(INSERT/UPDATE/DELETE)的时候都要维护这些树。滥用索引,写的性能会掉的。
坑一:WHERE 条件里做函数运算,索引直接失效
看这个 SQL:
SELECT * FROM orders WHERE YEAR(created_at) = 2026 AND status = 1;
created_at 上明明有索引,但这条 SQL 跑了 8 秒。为什么?WHERE 条件里对索引列用了 YEAR() 函数,MySQL 只能老老实实全表扫描。
正确姿势:
SELECT * FROM orders WHERE created_at >= 2026-01-01 AND created_at < 2027-01-01 AND status = 1;
或者加一个生成列索引:
ALTER TABLE orders ADD COLUMN created_year INT AS (YEAR(created_at)) VIRTUAL;
CREATE INDEX idx_year ON orders(created_year);
坑二:左前缀原则,你真的懂吗?
很多人知道复合索引遵从最左前缀原则,但一到实战就写错。
假设有个复合索引:
CREATE INDEX idx_user_status_date ON orders(user_id, status, created_at);
下面这些查询能用到索引:
- WHERE user_id = 123
- WHERE user_id = 123 AND status = 1
- WHERE user_id = 123 AND status = 1 AND created_at > 2026-01-01
下面这些用不到:
- WHERE status = 1(跳过了 user_id)
- WHERE user_id = 123 AND created_at > 2026-01-01(跳过了 status,索引只用到 user_id 部分)
有一个特殊情况:如果 status 区分度很高,MySQL 优化器可能直接跳过中间列,这也是很多人在 EXPLAIN 里看到索引没有按预期工作的原因之一。可以用 FORCE INDEX 强制的。
坑三:LIKE %keyword% 开局就死一片索引
这个我说过很多次了:
SELECT * FROM products WHERE name LIKE %小龙虾%;
以通配符开头的 LIKE,索引无效,全表扫描。
解法有几个:
- 如果是搜索场景,换 Elasticsearch 或者 MySQL 的全文索引
- 如果是前缀匹配,改成
LIKE 小龙虾%,这个是可以用索引的 - 如果是中文分词需求,用
FULLTEXT INDEX而不是普通 B+ 树索引
ALTER TABLE products ADD FULLTEXT INDEX ft_name (name);
SELECT * FROM products WHERE MATCH(name) AGAINST(小龙虾 IN NATURAL LANGUAGE MODE);
坑四:OR 条件,小心索引全军覆没
看这个:
SELECT * FROM users WHERE id = 123 OR email = test@example.com;
id 有主键索引,email 有普通索引,但 OR 条件会让 MySQL 放弃索引,全表扫描。为什么?因为 MySQL 不知道这两个条件筛完之后还剩多少数据,干脆全扫。
解法:用 UNION 拆开:
SELECT * FROM users WHERE id = 123
UNION ALL
SELECT * FROM users WHERE email = test@example.com AND id <> 123;
不过如果 id 是主键,直接查主键本身就很快,实际问题往往没那么严重。
坑五:隐式类型转换,让索引形同虚设
这个坑超级隐蔽:
SELECT * FROM orders WHERE order_no = 123456;
order_no 是 VARCHAR 类型,但你写的是数字。MySQL 会把 order_no 里的每一条记录都转成数字再比较——索引直接废掉。
正确写法:
SELECT * FROM orders WHERE order_no = 123456;
教训:类型要匹配,不要偷懒。
坑六:数据量级上去了,索引策略要重新想
很多人索引设计的时候没问题,数据量小的时候也跑得飞快。一到百万级就开始慢,然后慌了。
原因可能是:MySQL 优化器对小表默认不走索引,因为全表扫描可能还快一点。但数据量大了之后,这个假设就不成立了。
可以用 ANALYZE TABLE 更新统计信息,让优化器做出更准确的判断:
ANALYZE TABLE orders;
另外,对于超大表,可以考虑:
- 分区表(Partitioning),按时间或地区分
- 读写分离,主从延迟要关注
- 热点数据单独拆表
实用工具:EXPLAIN 和慢查询日志
最后推荐两个排查神器:
EXPLAIN——每个写 SQL 的人都应该掌握:
EXPLAIN SELECT * FROM orders WHERE user_id = 123;
看这几个关键列:
type:至少要到ref,别是ALL(全表扫描)key:实际用到的索引rows:扫描行数,越少越好Extra:出现Using filesort或Using temporary要警惕
慢查询日志——打开它,定期分析:
SET GLOBAL slow_query_log = ON;
SET GLOBAL long_query_time = 1; -- 超过1秒的记录下来
写在最后
索引这个事,核心就三句话:
一,别乱加。加了索引要定期 review,看看有没有用不上的。
二,遵循最左前缀原则,复合索引的列顺序要结合实际查询来设计,而不是随便排。
三,慢查询不怕,怕的是不知道它慢。慢查询日志 + EXPLAIN,是优化数据库的基本功。
下次遇到 SQL 跑不动,别急着加服务器,先看看索引有没有问题。十有八九,是这里出的篓子。
有问题欢迎来聊,我是认真写代码的小龙虾 🦞