Laravel大数据分页变慢的根本原因是paginate()默认使用OFFSET+LIMIT+COUNT(*),需扫全表;优化应绕开OFFSET、减少COUNT、避免全字段加载,并确保游标分页的排序字段唯一非空单调且有索引。

大数据量下 Laravel 分页变慢,根本原因不是代码写得不够“优雅”,而是 paginate() 默认走 OFFSET + LIMIT + COUNT(*) 这条路——数据库得扫完前面所有行才能给你第 100 页的 20 条数据。优化方向很明确:绕开 OFFSET、减少 COUNT、避免全字段加载。
cursorPaginate() 替换 paginate() 的硬性前提
游标分页不是“加个方法就行”,它依赖数据库层面的确定性排序和索引支撑:
- 必须显式调用
orderBy(),且字段要满足唯一、非空、单调(如id或created_at加 microsecond) -
created_at单独用风险高——同一秒多条记录时顺序不稳定,容易漏或重复;稳妥做法是复合游标:orderBy('created_at', 'id') - 对应字段必须有数据库索引,否则 WHERE 条件推进时仍会全表扫描
- 不支持
with()预加载,因为游标逻辑只作用于主表;需要关联数据时,前端分批请求或改用JOIN手动拼 - API 响应里必须透出
next_cursor,前端下次请求要原样带上,不能自己拼、不能 decode 后改
Fast Paginate 在关联查询中怎么用才不翻车
它对关联分页确实友好,但不是无脑替换:
- 安装后直接调用
fastPaginate()或simpleFastPaginate(),前者返回总条数,后者不查COUNT、性能更高 - 在
HasManyThrough或BelongsToMany关系里也能用,比如$user->roles()->fastPaginate(10),它会自动处理中间表 - 别在已用
with()的链路上再套fastPaginate()——预加载本身可能引入 N+1 或冗余 JOIN,先确保关联查询本身是干净的 - 如果模型启用了全局作用域(Global Scope),
fastPaginate()会继承,但某些自定义作用域可能干扰内层 ID 查询,建议在测试环境验证结果一致性
缓存 COUNT 和整页响应的取舍边界
缓存不是万能解药,用错地方反而增加复杂度:
-
COUNT缓存只适合数据变更频率低的场景(比如后台字典表、分类列表),缓存键建议用md5($query->toSql() . json_encode($query->getBindings())),过期时间设 30 分钟足够 - 整页响应缓存(HTML/JSON)只适用于完全静态或极少更新的数据,且必须包含完整 URL 参数哈希(如
sha1($request->fullUrl())),否则带不同search参数的请求会命中错误缓存 - 用户登录态、权限相关分页绝对不能缓存整页——哪怕加了
auth中间件,也要在缓存键里显式加入Auth::id() - 缓存
LengthAwarePaginator实例本身意义不大,因为 Paginator 对象不序列化模型数据,真正耗时的是数据库查询,缓存它不如缓存查询结果数组
真正卡住的从来不是分页语法,而是排序字段没索引、游标值被前端篡改、COUNT 缓存键没包含绑定参数、或者以为加了缓存就能无视数据一致性。每一步优化都得落在数据库执行计划和实际请求链路上,而不是堆工具。


















