查得慢八成因sql未走索引,应先用db::select('explain')分析执行计划;type为all即全表扫描,where字段无索引需立即添加,复合查询建联合索引时等值条件在前、范围条件在后。

查得慢,八成不是 Eloquent 写得丑,而是 SQL 没走索引。别急着改模型关系或加缓存,先让数据库告诉你它在干什么。
EXPLAIN 之前别动代码
用 DB::select('EXPLAIN SELECT ...') 看执行计划,而不是靠 toSql() 猜——后者不带绑定值,WHERE email = 'a@b.c' 和 WHERE email = ? 在 MySQL 看来可能是两条完全不同的执行路径。
-
type是ALL?说明全表扫描,立刻检查 WHERE 字段有没有索引 -
key为空?哪怕有索引也可能没被选中,注意隐式类型转换(比如字符串字段查整数)或函数包裹(WHERE DATE(created_at) = '2024-01-01')会直接让索引失效 - 复合查询如
WHERE status = ? AND created_at > ?,联合索引必须是(status, created_at),反过来就只用上第一个字段
with() 不是银弹,嵌套深了反而更慢
->with(['posts.comments.author']) 看起来干净,但默认生成的是 N+1 式多条 IN 查询,中间层一多,PHP 进程内存和反序列化开销远超数据库连接成本。
- 适用场景很窄:主表结果 ≤ 100 行、关联表有对应外键索引、层级 ≤ 2
- 深层嵌套优先考虑
join()+ 手动映射,或换hasManyThrough这类原生关系 - 只要总数不要详情?用
withCount(),比with()轻量得多 - 想查「每个用户最新一条订单」?别用闭包限制
->with(['orders' => fn($q) => $q->latest()->limit(1)]),这会为每个用户跑一次子查询;改用窗口函数或JOIN ... ON orders.id = (SELECT MAX(id) FROM orders o2 WHERE o2.user_id = users.id)
chunkById() 比 chunk() 稳,但事务里不能用
chunk(1000) 依赖 id BETWEEN ? AND ?,一旦中间删过记录,就会漏数据或重复处理;chunkById(1000) 改用 id > ? ORDER BY id LIMIT 1000,天然规避空洞问题。
- 仅适用于自增主键;UUID 主键得按
created_at分段,并给(deleted_at, created_at)加覆盖索引 -
chunkById()不支持事务内使用——它靠多次独立查询推进,事务里会锁住前一批结果,导致后续查询阻塞 - 软删除字段
deleted_at没索引?WHERE deleted_at IS NULL同样触发全表扫描;建议建(deleted_at, id)覆盖索引
DataTables 分页别硬算总条数
大数据集下 COUNT(*) 可能比主查询还慢。Laravel DataTables 的 skipTotalRecords() 不是偷懒,而是合理取舍。
- 只显示「当前页 1–10 条,共 10 条」?
skipTotalRecords()直接把recordsTotal设为和recordsFiltered一致,跳过 COUNT - 搜索条件复杂时,Eloquent DataTable 会自动把
select *推到子查询里,再套一层COUNT(*)——务必配合select('id', 'name', ...)显式指定字段 - 如果必须显示准确总数,且数据稳定,可缓存
Cache::remember('users_total', 3600, fn() => User::count()),但注意缓存失效时机
最常被忽略的点:索引不是加了就完事。MySQL 8.0+ 支持 ALTER TABLE t ALTER COLUMN c SET INVISIBLE,测试时临时关掉某个索引看影响,比猜强得多。











