加索引需精准匹配查询模式:where高频字段、外键、softdeletes字段须建索引;复合索引遵循最左匹配,等值条件在前、范围条件在后;unique()兼具约束与索引,index()仅加速查询;命名索引便于管理,迁移需手动增删,验证须结合explain与慢日志。

加索引不是“写个 index() 就完事”,关键得看字段用在哪、怎么查、MySQL 实际认不认。线上慢查没改善,八成是索引建错位置、顺序不对,或者根本没被命中。
WHERE 条件字段不加索引,查询基本等于全表扫描
比如 SELECT * FROM orders WHERE user_id = 123 耗时 2 秒,EXPLAIN 显示 type: ALL,说明完全没走索引。这种场景必须加:
- 所有高频出现在
WHERE中的字段,如user_id、status、email - 外键字段(如
user_id)必须手动加索引——foreignId()默认不建索引,只建约束 -
softDeletes()字段(deleted_at)如果常用于whereNull('deleted_at')或whereNotNull,也建议加 - 字符串字段要控长度:
$table->string('description', 500)->index()->length(191),否则 MySQL 可能静默失效
复合索引字段顺序错了,等于白建
建了 ['status', 'created_at'] 索引,但查询是 WHERE created_at > ? AND status = ?,EXPLAIN 很可能显示没走索引。原因:B-tree 索引依赖最左匹配。
- 等值条件(
=、IN)放前面,范围条件(>、BETWEEN、LIKE 'abc%')放后面 - 排序字段(
ORDER BY created_at DESC)适合放在复合索引末尾,但前提是前面的等值条件已覆盖 - 显式命名索引更稳妥:
$table->index(['user_id', 'status'], 'orders_user_status_idx'),后续删索引用dropIndex('orders_user_status_idx')不容易出错 - 别把
created_at放第一位再配user_id——除非你真有大量按时间查全表的需求
unique() 和 index() 别混着用,语义和效果完全不同
很多人以为 $table->string('email')->index() 能防重复,结果上线后邮箱撞车。这是典型误用。
-
unique()是约束 + 索引,插入重复值直接报Integrity constraint violation;index()只加速查询,不拦数据 - 邮箱、订单号这类需唯一性的字段,必须用
$table->string('email')->unique(),不用额外加index() - 软删除场景下,单字段
unique('email')不够——已软删的用户还占着邮箱,新用户注册会冲突;得用复合唯一:$table->unique(['email', 'deleted_at']) - 已有重复数据时,
migrate会直接失败;先用SELECT email, COUNT(*) FROM users GROUP BY email HAVING COUNT(*) > 1查重,再清理
迁移回滚不删索引,下次 migrate 容易报 “index already exists”
Laravel 默认的 down() 方法只删表,不删索引。如果你没手动写 dropIndex(),索引就一直挂着。
- 建索引时务必记下名称,比如
$table->index('email', 'users_email_idx') - 对应
down()里补上:$table->dropIndex('users_email_idx') - 查现有索引用原生 SQL:
DB::select('SHOW INDEX FROM users'),看Key_name和Column_name是否符合预期 - 别依赖 Laravel 自动生成的索引名(如
users_email_index),不同版本或环境可能不一致
真正难的不是写那行 index(),而是搞清查询实际走哪条路径、MySQL 有没有用上、以及加完之后写入变慢了多少——这些没法靠迁移文件自动告诉你,得靠 EXPLAIN、慢日志和真实流量验证。











