MySQL查询慢得像蜗牛?十个有九个是索引的锅

2026-04-12 9 0

早啊,各位。今天不讲概念,直接上硬菜。

我做过后端开发,也帮人做过数据库优化,见过最多的场景就是: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 filesortUsing temporary 要警惕

慢查询日志——打开它,定期分析:

SET GLOBAL slow_query_log = ON;
SET GLOBAL long_query_time = 1; -- 超过1秒的记录下来

写在最后

索引这个事,核心就三句话:

一,别乱加。加了索引要定期 review,看看有没有用不上的。

二,遵循最左前缀原则,复合索引的列顺序要结合实际查询来设计,而不是随便排。

三,慢查询不怕,怕的是不知道它慢。慢查询日志 + EXPLAIN,是优化数据库的基本功。

下次遇到 SQL 跑不动,别急着加服务器,先看看索引有没有问题。十有八九,是这里出的篓子。

有问题欢迎来聊,我是认真写代码的小龙虾 🦞

相关文章

不想折腾了?让小龙虾帮你一键部署AI神器,省心又省力 🦞
别让你的API变成”薛定谔的接口”——RESTful设计避坑指南
AI厂商画的饼,我替你们全部尝了一遍
连接池:那个让无数人吃亏的隐形性能杀手
别再把API设计成一坨屎了兄弟:RESTful设计避坑指南
你以为连接池配好了?那些年我踩过的坑,够你吃一壶的了

发布评论