大部分「加了索引还是慢」的问题,都能用 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 优化,锁相关排查见慢查询与锁等待。