大部分「加了索引还是慢」的问题,都能用 EXPLAIN 在十秒内判断出来:要么索引没用上,要么用上了但回表次数太多。

MySQL 索引设计:最左前缀、覆盖索引与 EXPLAIN 的正确读法 示意图

联合索引的顺序就是筛选顺序

ALTER TABLE orders
    ADD INDEX idx_user_status_created (user_id, status, created_at);

B+ 树先按 user_id 排序,相同再按 status,最后按 created_at。所以查询条件必须从最左列开始连续匹配,这就是最左前缀原则:

  • where user_id = 1:能用;
  • where user_id = 1 and status = 'paid':能用,走两列;
  • where status = 'paid':用不上,跳过第一列等于放弃排序;
  • where user_id = 1 and created_at > '2026-01-01':只有 user_id 参与定位,created_at 无法继续用于缩小范围。

范围条件之后的列会失去定位能力(仍可用于覆盖索引取值),所以设计时等值条件放前面,范围条件放最后。

EXPLAIN 只看四列就够

EXPLAIN SELECT id, total
FROM orders
WHERE user_id = 42 AND status = 'paid'
ORDER BY created_at DESC
LIMIT 20;
  • type:const、eq_ref、ref、range 可接受,出现 ALL 就是全表扫描;
  • key:实际使用的索引,为空说明没走索引;
  • rows:预估扫描行数,与结果集相差很大说明选择性差;
  • Extra:出现 Using filesort 或 Using temporary 要重点看,Using index 表示用上了覆盖索引。

覆盖索引:不回表才是真的快

二级索引的叶子节点存的是主键,查其他列必须回主键索引再取一次,这叫回表。如果查询需要的列都在索引里,就能直接从索引返回:

SELECT user_id, status, created_at
FROM orders
WHERE user_id = 42;

列表页常见的「先用索引查 id,再用主键批量取详情」也是同一思路:一次索引扫描拿到 20 个主键,再按主键范围查询,避免逐条回表。

索引失效的几种典型写法

  • 在索引列上做函数或运算:where date(created_at) = '2026-10-01',改成范围比较;
  • 隐式类型转换:字符串列与数字比较,索引直接失效;
  • 前导模糊匹配:like '%keyword',考虑全文索引或搜索引擎;
  • or 连接了未建索引的列,优化器可能直接放弃索引。

索引不是越多越好

每个索引都要在写入时维护,还要占用缓冲池。实践判断:单表索引控制在 5 个以内,联合索引优先于大量单列索引,低选择性字段(性别、状态)不要单独建索引。上线前用接近真实的数据量跑一次 EXPLAIN,比在开发库上凭数据量猜可靠得多。

小结

索引设计服务于具体查询:确定 where 与 order by 的列 → 等值在前、范围在后 → 用 EXPLAIN 验证 type 与 Extra。查询条数层面的问题(N+1)见Eloquent 的 N+1 优化,锁相关排查见慢查询与锁等待。