Using index消失主因是索引覆盖未达成,即SELECT、JOIN ON、WHERE涉及的列未被同一索引全包含且满足最左前缀;多表JOIN时易遗漏被驱动表连接键或非连接字段,需建以连接键开头的复合索引并确保类型、字符集严格一致。

EXPLAIN 显示 Using index 消失,但连接字段明明有索引
这通常不是索引没建,而是「索引覆盖」没达成。MySQL 的 Using index 要求:查询中所有用到的列(SELECT + JOIN ON + WHERE)都必须被同一个索引「全包含」,且顺序满足最左前缀。多表 JOIN 时,驱动表和被驱动表的字段混在一起,很容易漏掉某张表的连接键。
常见错误现象:EXPLAIN FORMAT=TREE 中某张表的 Extra 是 Using where; Using join buffer 或空,而不是 Using index;key 列显示用了索引,但 rows 很大。
- 先确认哪张表丢了覆盖:对每张参与 JOIN 的表单独看
EXPLAIN行,重点盯table、key、Extra三列 - 被驱动表的连接字段(如
t2.user_id)如果只在单列索引里,而 SELECT 又取了t2.name和t2.status,那这个单列索引就无法覆盖——它缺了name和status - 复合索引必须把「连接键放最左」,再接查询需要的其他列。例如驱动表条件是
WHERE t1.status = 'active',JOIN 条件是ON t1.id = t2.t1_id,而你要查t2.name,那就得建INDEX idx_t2_t1_id_name (t1_id, name),不能反过来
ALTER TABLE 添加复合索引时,字段顺序怎么定
顺序不是按 SQL 里写的先后,而是按「访问模式」:连接键必须在最左边,WHERE 条件列次之,SELECT 列最后。否则最左前缀一断,后面全失效。
比如这条查询:
SELECT t2.name, t2.created_at FROM t1 JOIN t2 ON t1.id = t2.t1_id WHERE t1.status = 'done' AND t1.updated_at > '2026-01-01';
它对 t2 的访问模式是:先用 t2.t1_id 定位行,再读 name 和 created_at。所以 t2 上的索引必须是:
-
INDEX idx_t2_t1_id_name_created_at (t1_id, name, created_at)✅ -
INDEX idx_t2_name_t1_id (name, t1_id)❌ ——t1_id不在最左,JOIN 时无法用上 -
INDEX idx_t2_t1_id_created_at (t1_id, created_at)⚠️ —— 覆盖了created_at,但没覆盖name,仍要回表
为什么加了复合索引,EXPLAIN 还不显示 Using index
即使索引字段全了,也可能因隐式转换或字符集不一致,让 MySQL 放弃使用该索引做覆盖。这时候 key 列可能还显示索引名,但 Extra 里没有 Using index。
- 检查
t1.id和t2.t1_id的字符集与排序规则是否完全一致:SHOW FULL COLUMNS FROM t2 LIKE 't1_id',对比Collation值 - 确认数据类型严格匹配:都是
BIGINT UNSIGNED,而不是一边INT一边VARCHAR(20);后者会触发CAST(t2.t1_id AS SIGNED),覆盖直接失效 - 避免在连接字段上用函数:
ON t1.id = CAST(t2.t1_id AS UNSIGNED)或ON t1.id = CONCAT('', t2.t1_id)都会让索引退化为普通查找 - 如果表有
TEXT或BLOB字段被 SELECT,即使加进索引也无效——这类字段不能建前缀索引以外的索引,更无法放进覆盖索引
覆盖索引太宽影响写性能,要不要加
加,但得权衡。每多一个字段进复合索引,INSERT/UPDATE/DELETE 就多一次 B+ 树更新,索引体积也增大。对高频写入表,别无脑堆字段。
- 只把「必然出现在该 JOIN 查询里的 SELECT 列」加进去,不要提前预设“以后可能要用”
- 如果同一张表被多个 JOIN 查询用到,优先覆盖最重的那条(QPS 最高 or rows 扫描最多)
- 用
INFORMATION_SCHEMA.STATISTICS查当前索引大小:SELECT index_name, seq_in_index, column_name FROM INFORMATION_SCHEMA.STATISTICS WHERE table_name = 't2' ORDER BY index_name, seq_in_index,确认新加字段没导致索引膨胀超预期 - 特别注意时间字段:如果
created_at是DATETIME(6),它占 8 字节;而TIMESTAMP只占 4 字节,选型时就影响索引宽度
真正容易被忽略的是:覆盖索引生效的前提,是优化器认为走索引比走主键聚簇索引更便宜。如果被驱动表返回行数太多(比如 > 20% 总行数),MySQL 可能主动放弃覆盖索引,改用主键回表——这时得靠限制结果集或拆分查询来治本。










