explain显示type=all说明mysql正在全表扫描,需立即检查索引缺失、字段类型不匹配、联合索引未满足最左前缀、隐式转换、函数操作或低选择性导致优化器放弃索引。

线上数据库查询变慢、CPU持续飙升、分页卡顿,八成是索引没配对或根本没建——你改了十遍 Eloquent 关系、加了三层缓存,结果 EXPLAIN 里 still type=ALL,MySQL 正在扫描 180 万行用户数据。
先让数据库自己开口说话
别猜,别改模型,直接在生产环境 Tinker 或临时路由里执行带参数绑定的 EXPLAIN:
【DB::select('EXPLAIN SELECT * FROM orders WHERE user_id = ? AND status = ?', [123, 'shipped'])】
这一步必须用 DB::select('EXPLAIN ', []),不能用 toSql()——后者把 ? 替换成字符串后,MySQL 看到的是常量值,而真实查询走的是预编译参数,执行路径可能完全不同。
紧盯三列:type 至少要是 ref;key 必须显示索引名,NULL 就等于没走;rows 如果接近总行数(比如 100 万行扫了 92 万),说明索引失效或选择性极差。
高频字段补索引的实操路径
只给真正出现在 WHERE、JOIN、ORDER BY 中的字段加索引。外键、软删除字段、状态+时间组合,这三类不加等于默认开全表扫描。
方法一:单字段索引(外键/邮箱/手机号)
生成迁移:php artisan make:migration add_index_to_orders_user_id
编辑 up() 方法:$table->index('user_id', 'orders_user_id_idx');
运行迁移:php artisan migrate
方法二:复合索引(等值前置,范围后置)
常见场景:WHERE status = ? AND created_at > ?
必须写成:$table->index(['status', 'created_at'], 'orders_status_created_idx');
【顺序不能颠倒】——如果写成 ['created_at', 'status'],MySQL 只能用上 created_at,status 完全失效。
方法三:软删除字段必须索引
只要用了 whereNull('deleted_at') 或 withTrashed(),deleted_at 就得加索引。
推荐覆盖索引:$table->index(['deleted_at', 'id'], 'versions_deleted_at_id_idx');
这样既能支撑 WHERE deleted_at IS NULL,又能配合后续按 id 分页,避免 filesort。
生产环境加索引的避坑操作
第一步:确认 MySQL 版本与存储引擎
执行 SELECT VERSION(), @@default_storage_engine;,InnoDB 对索引长度限制严格,VARCHAR(255) 默认只索引前 768 字节,utf8mb4 下必须显式指定前缀:$table->index('email(191)', 'users_email_191_idx');
第二步:禁用直接迁移加索引
生产环境千万不能直接跑 php artisan migrate 加索引——大表会锁表数分钟甚至更久。
改用 pt-online-schema-change 工具在线加索引:pt-online-schema-change --alter "ADD INDEX idx_user_status (user_id, status)" D=your_db,t=orders --execute
第三步:验证索引是否生效
加完立刻再跑一遍 EXPLAIN,确保 key 显示刚建的索引名,type 从 ALL 变成 ref,rows 从百万级降到几百或几千。
第四步:清理无用索引
查出未被使用的索引:SELECT * FROM sys.schema_unused_indexes WHERE object_schema = 'your_db';
用迁移删掉它:$table->dropIndex('old_unused_idx');











