应优先优化 n+1 查询和重复加载而非盲目加索引;用 telescope 或 db::listen 定位问题,合理预加载、精简字段、避免 toarray/tojson 开销,注意软删除全局作用域与索引,游标分页需唯一排序字段。

什么时候该考虑优化 Eloquent 查询,而不是硬加索引?
多数性能问题根本不是数据库慢,而是 N+1 查询或重复加载同一模型。比如在循环里调用 $user->posts,没用 with() 预加载,就会触发几十次 SQL —— 这比加个 INDEX 有用十倍。
实操建议:
- 先用 Laravel Telescope 或
DB::listen()看实际执行了哪些查询,确认是不是 N+1 或重复SELECT * - 只对真正被频繁访问的关联字段做预加载,别无脑
with(['posts', 'profile', 'settings'])—— 多余字段会拖慢序列化和内存占用 - 如果只是取计数,用
$user->posts()->count(),别先get()再count()
toArray() 和 toJson() 为什么让 API 变慢?
它们会强制遍历所有 Attribute、Cast、Appends,还会递归处理关联模型 —— 即使你只想要 id 和 name。
实操建议:
- API 返回固定字段时,优先用
select('id', 'name', 'email')+get(),再手动map()成数组,跳过 Eloquent 的序列化开销 - 避免在
toArray()中调用耗时逻辑(比如文件路径拼接、远程 API 调用),这类逻辑应移到资源类(Resource)或服务层 - 如果用了
$casts且字段值是 JSON 字符串,注意它会在每次toArray()时重复json_decode—— 大量数据下明显卡顿
软删除(SoftDeletes)不加 WHERE deleted_at IS NULL 就等于没删
启用 SoftDeletes 后,Eloquent 默认不会自动过滤已软删除记录 —— 除非你显式调用 withTrashed() 或 onlyTrashed(),否则 find()、first()、get() 全部忽略该约束。
实操建议:
- 全局作用域才是关键:在模型中定义
protected static function booted()并调用static::addGlobalScope(new SoftDeletingScope())(Laravel 默认已注册,但自定义查询如DB::table()或原生 SQL 不受控) - 写原生查询或使用
DB::select()时,必须手写AND deleted_at IS NULL,否则软删除形同虚设 - 迁移里加
$table->softDeletes()后,记得给deleted_at字段加索引:$table->index('deleted_at'),否则带软删除的分页会越来越慢
用 cursorPaginate() 替代 paginate() 的真实代价
cursorPaginate() 确实能跳过 OFFSET 性能陷阱,但它要求排序字段严格唯一、非空、有索引 —— 比如用 id 可以,用 created_at 就容易翻车(毫秒级重复)。
实操建议:
- 必须确保
orderBy字段组合能唯一确定一行(推荐orderBy('id')或orderBy('created_at', 'id')) - 不能混用
where和模糊条件(如LIKE)后还指望游标稳定 —— 索引失效会导致结果错乱或漏数据 - 前端传来的
cursor是 Base64 编码的上一页最后一条记录的排序字段值,别把它当 ID 解析;解码后要校验格式,防止注入或空指针
最常被忽略的一点:游标分页不支持跳转到任意页码(比如“第 23 页”),也不支持 lastPage() —— 如果业务强依赖这些,硬切反而增加复杂度。











