Laravel 默认不支持游标分页,因paginate()依赖COUNT和OFFSET导致大数据量下性能骤降;游标分页通过排序字段值锚定下一页,避免深度扫描,需手动实现或使用laravel/cursor-paginator包。
游标分页不是 Laravel 默认提供的功能,
和
都基于 offset/limit,大数据量下性能会断崖式下降。真要游标分页,得自己实现或用第三方包。
为什么不能直接用 paginate() 做游标分页
Laravel 的
必须执行 COUNT 查询来算总页数,且依赖
跳转——当页码很大(比如第 10000 页)时,MySQL 要扫描前 150000 行才能拿到数据,I/O 和 CPU 开销爆炸。游标分页绕开 offset,靠「上一页最后一条记录的排序字段值」作为下一页起点,天然避免深度扫描。
在 page=10000 时实际执行
,效率取决于索引覆盖和排序稳定性
没有内置
方法;Laravel 官方至今未提供游标分页原生支持(截至 2026 年 4 月)
试图对
或
“魔改”实现游标逻辑,大概率破坏分页器契约,导致
、
等行为异常
手动实现游标分页的关键三步
核心是:用唯一、有序、非空的字段(通常是
或
)做游标锚点,每次查询只取“比上一页末尾值更大”的一批记录。
前端首次请求不带游标参数,后端查第一页:按排序字段倒序取前
条,例如
返回响应时,把最后一条记录的
(或组合字段如
)作为
字段传给前端
前端下次请求带上
,后端构造查询:
(注意是
,不是
,避免重复)
必须为游标字段加数据库索引,否则 WHERE + ORDER BY 会全表扫描;若用复合游标(如
),索引需匹配顺序
laravel
/cursor-paginator 包怎么用
社区最成熟的方案是
(由 Laravel 核心贡献者维护),它提供
类,行为接近原生分页器但无 offset。
Laravel 13.2.0
PHP中文网提供Laravel 13.2.0版本下载,Laravel框架 是基于 PHP 8.3+ 的高性能框架,官方推荐通过 Composer 安装。它内置 AI SDK、JSON:API Resources 及原生向量搜索,支持属性驱动开发与队列路由,大幅提升开发效率。相比旧版,13.2.0 优化了缓存 TTL 管理与实时通信,无需 Redis 即可横向扩展。作为现代 Web 开发首选,它兼顾安全与极速体验,助您快速构建企业级应用。
下载
安装:
用法示例(控制器中):
返回对象是
实例,有
、
、
、
等方法,但没有
或
—— 这是游标分页的设计使然
Blade 中不能直接用
,因为默认视图依赖总页数;需手写链接逻辑或自定义视图,例如:
API 场景下容易忽略的边界问题
游标分页在 API 中更常见,但几个细节不处理好就会翻车:
游标值必须 URL-safe,
或
处理后再传给前端,尤其当游标是时间戳+ID组合时
不能信任客户端传来的游标值,必须校验格式并防 SQL 注入;建议用
而非字符串拼接
排序字段必须严格唯一且不可为空,否则会出现漏数据或重复;若业务允许,优先用自增
,慎用
(毫秒级不保证唯一)
前端“加载更多”时,即使
为 true,也要检查返回的
数组长度是否为 0,防止无限请求空结果
游标分页真正难的不是代码怎么写,而是设计阶段就要想清楚:排序依据是否稳定、游标如何编码传输、前端如何管理 cursor 状态、缓存策略怎么适配——这些一旦定错,后期几乎无法平滑迁移。
paginate()simplePaginate()paginate()OFFSETpaginate(15)LIMIT 149985, 15cursorPaginate()LengthAwarePaginatorSimplePaginatorlinks()hasMorePages()idcreated_at$perPageSELECT * FROM posts ORDER BY id DESC LIMIT 15idid, created_atnext_cursor?cursor=12345SELECT * FROM posts WHERE id < 12345 ORDER BY id DESC LIMIT 15<<=created_at, idlaravel/cursor-paginatorCursorPaginatorcomposer require laravel/cursor-paginator$cursor = $request->input('cursor');
$posts = Post::where('status', 'published')
->orderBy('id', 'desc')
->cursorPaginate(15, ['*'], 'cursor', $cursor);CursorPaginatoritems()nextCursor()previousCursor()hasMorePages()total()lastPage()$posts->links()nextCursor() }}">下一页base64_encode()urlencode()Post::query()->whereRaw('id > ?', [$cursor])idcreated_athasMorePages()data