高频where/order by/join字段、外键、软删除deleted_at、复合查询组合(如[deleted_at, created_at])必须加索引;左模糊like无效;用explain验证,type至少ref、key非空、rows显著小于总数。

哪些字段必须加数据库索引?
Laravel 本身不自动加索引,全靠你判断。高频出现在 WHERE、ORDER BY、JOIN 条件里的字段,不加索引就是慢查询的起点。
- 外键字段(如
user_id、post_id)几乎 always 需要单独索引
- 联合查询常用组合(如
status + created_at)建议建复合索引,顺序按选择性高 → 低排列(比如 status 值少,放前面反而降低效率)
-
LIKE '%xxx' 这种左模糊查询,B-tree 索引无效,别白加
- Laravel 的软删除字段
deleted_at,如果经常查 ->withTrashed() 或 ->onlyTrashed(),它得单独建索引,否则全表扫描
如何验证索引是否生效?
别猜,用 EXPLAIN 看执行计划。在 Tinker 或数据库客户端里直接跑带 EXPLAIN 的原始 SQL,重点看这几列:
-
type:至少要是 ref 或 range,ALL 就是全表扫
-
key:显示实际用到的索引名,空值 = 没走索引
-
rows:预估扫描行数,比总行数小一个数量级才算有效
user_id、post_id)几乎 always 需要单独索引 status + created_at)建议建复合索引,顺序按选择性高 → 低排列(比如 status 值少,放前面反而降低效率) LIKE '%xxx' 这种左模糊查询,B-tree 索引无效,别白加 deleted_at,如果经常查 ->withTrashed() 或 ->onlyTrashed(),它得单独建索引,否则全表扫描 EXPLAIN 看执行计划。在 Tinker 或数据库客户端里直接跑带 EXPLAIN 的原始 SQL,重点看这几列:
-
type:至少要是ref或range,ALL就是全表扫 -
key:显示实际用到的索引名,空值 = 没走索引 -
rows:预估扫描行数,比总行数小一个数量级才算有效
Laravel 的 DB::enableQueryLog() 只记 SQL,不记执行计划,得手动补 EXPLAIN。线上环境慎用 EXPLAIN ANALYZE(会真实执行),开发环境够用。
迁移中加索引的常见翻车点
Laravel 迁移写 $table->index('column') 很顺手,但生产环境容易卡住甚至锁表。
- MySQL 5.6+ 支持在线 DDL,但加索引仍会阻塞写入(尤其大表),务必在低峰期操作
- PostgreSQL 更友好,但
CONCURRENTLY 不能在事务里用,Laravel 迁移默认包事务,得手动关掉:DB::unprepared('CREATE INDEX CONCURRENTLY ...')
- 字符串字段超 191 字节(比如
utf8mb4 下的 VARCHAR(255)),MySQL 默认不支持全文索引或前缀索引以外的索引,得显式指定长度:$table->index('description', null, 'fulltext')->fulltext() 或 $table->index('email(191)')
软删除 + 时间范围查询怎么避免索引失效?whereNotNull('deleted_at') 和 whereBetween('created_at', [...]) 同时出现时,单列索引大概率失效。
- 不要用两个单列索引硬拼,MySQL 一般只选其一;优先建复合索引,把
deleted_at 放前面(因它是等值判断),再跟时间字段:['deleted_at', 'created_at']
- 如果业务上
deleted_at IS NOT NULL 实际等价于 deleted_at > 0,可考虑把该字段设为 UNSIGNED INTEGER 并加默认值,避免 NULL 值干扰索引选择
- Eloquent 中用
whereNotNull() 生成的 SQL 是 IS NOT NULL,而 where('deleted_at', '!=', null) 是错的(PHP null 会被转成空字符串),这点容易写错
CONCURRENTLY 不能在事务里用,Laravel 迁移默认包事务,得手动关掉:DB::unprepared('CREATE INDEX CONCURRENTLY ...') utf8mb4 下的 VARCHAR(255)),MySQL 默认不支持全文索引或前缀索引以外的索引,得显式指定长度:$table->index('description', null, 'fulltext')->fulltext() 或 $table->index('email(191)') whereNotNull('deleted_at') 和 whereBetween('created_at', [...]) 同时出现时,单列索引大概率失效。
- 不要用两个单列索引硬拼,MySQL 一般只选其一;优先建复合索引,把
deleted_at放前面(因它是等值判断),再跟时间字段:['deleted_at', 'created_at'] - 如果业务上
deleted_at IS NOT NULL实际等价于deleted_at > 0,可考虑把该字段设为UNSIGNED INTEGER并加默认值,避免 NULL 值干扰索引选择 - Eloquent 中用
whereNotNull()生成的 SQL 是IS NOT NULL,而where('deleted_at', '!=', null)是错的(PHPnull会被转成空字符串),这点容易写错
索引不是加得越多越好,每多一个索引,写操作就多一份维护成本,特别是高频更新的表。上线前一定用真实数据量压测,别信本地 10 行测试结果。











