百万级数据查询慢主因是索引未“兜住”WHERE、ORDER BY或SELECT字段;需按最左前缀原则建复合索引,并优先覆盖查询所需列以避免回表。

百万级数据查得慢,八成不是服务器扛不住,而是 WHERE、ORDER BY 或 SELECT 的字段没被索引“兜住”。光建单列索引不够,得看查询模式配复合索引,再看是否能省掉回表——也就是覆盖索引。
复合索引的字段顺序为什么不能乱
MySQL用B+树索引,只能高效利用「最左前缀」。比如你建了 INDEX idx_user_status_time (status, created_at, user_id),那下面这些查询能走索引:
WHERE status = 1WHERE status = 1 AND created_at > '2025-01-01'WHERE status = 1 AND created_at = '2025-01-01' AND user_id = 123
但这些就用不上或只能部分用:
-
WHERE created_at > '2025-01-01'(跳过status,最左失效) -
WHERE user_id = 123(完全没碰最左字段) -
WHERE status = 1 AND user_id = 123 ORDER BY created_at(ORDER BY字段不在索引连续后缀里,可能触发 filesort)
实操建议:把等值过滤字段放最左,范围查询字段(>、BETWEEN、LIKE 'abc%')放中间,ORDER BY 和 GROUP BY 字段尽量接在后面且顺序一致。
怎么判断一个查询是否命中覆盖索引
覆盖索引指:查询所需的所有字段,全部包含在同一个索引中,无需回主键聚簇索引捞数据行。这时 EXPLAIN 的 type 是 index 或 range,且 Extra 显示 Using index。
比如表有字段 id、user_id、status、created_at、amount,你常跑:
SELECT user_id, status, amount FROM orders WHERE user_id = 1001 AND status = 1;
那就建复合索引:INDEX idx_uid_status_amount (user_id, status, amount)。注意 amount 放最后——它不参与过滤,只用于“覆盖”,放前面反而浪费索引空间和写入开销。
容易踩的坑:
- 把
TEXT或VARCHAR(2000)字段塞进索引,会导致索引体积暴增、写入变慢 - 以为
SELECT *能走覆盖索引——不可能,主键以外的字段全在聚簇索引里,必须显式列出需要的列 - 忽略
WHERE中隐式类型转换,比如user_id是INT,却传字符串'1001',索引直接失效
哪些场景下复合索引比多个单列索引更有效
当查询条件含多个字段组合时,MySQL一般只用一个单列索引(选 rows 预估最少的那个),其余条件靠扫描过滤。而复合索引能把多个条件“打包”进一棵B+树。
典型例子:
- 后台订单列表页:
WHERE shop_id = ? AND status IN (1,2,3) AND created_at > ? ORDER BY created_at DESC - 用户行为统计:
WHERE app_id = ? AND event_type = ? AND dt BETWEEN ? AND ?
这种场景下,INDEX (shop_id, status, created_at) 比分别建三个单列索引快得多——后者最多只用上 shop_id,剩下两个字段全靠临时结果集过滤,I/O翻倍。
但注意:如果业务中同时高频运行 WHERE status = ? 和 WHERE created_at > ? 这类单独查询,那单列索引仍有存在价值,别一刀切删掉。
覆盖索引对 ORDER BY 和分页的影响
只要 ORDER BY 字段是复合索引的后缀,且顺序匹配(ASC/DESC 也要一致),就能避免 Using filesort;如果还把 SELECT 字段都放进索引,就是双重优化。
比如分页查最新10条订单:
SELECT id, user_id, amount FROM orders WHERE status = 1 ORDER BY created_at DESC LIMIT 10;
建索引:INDEX idx_status_ctime_id_amt (status, created_at, id, amount)。
这里 id 和 amount 是为了覆盖,created_at 是为了排序,status 是过滤入口。注意 created_at 必须是 DESC ——MySQL 8.0+ 支持混合升序降序索引,但 5.7 及以前只支持全 ASC 或全 DESC,否则仍会 filesort。
真正容易被忽略的一点:分页深度大时(如 LIMIT 10000, 10),即使有索引,也要先定位到第10001行,再取10条。这时候覆盖索引能减少磁盘随机读,但不如改用游标分页(WHERE created_at < ? ORDER BY created_at DESC LIMIT 10)来得彻底。


















