SQL 如何实现“游标分页”(keyset pagination)避免深分页慢

发布时间 - 2026-01-29 00:00:00    点击率:
keyset pagination 是一种基于排序字段值的游标分页方法,比 OFFSET 更快,因其跳过扫描直接定位;要求排序字段具唯一性,SQL 通过比较上一页末记录的 (created_at, id) 值实现高效分页。

什么是 keyset pagination,为什么比 OFFSET 快

OFFSET 分页在数据量大、页码靠后时会越来越慢,因为数据库必须扫描并跳过前面所有行。keyset pagination(又称游标分页)不依赖行号,而是用上一页最后一条记录的排序字段值作为下一页的起点,跳过扫描过程,直接定位。

它本质是“基于值的分页”,要求排序字段有唯一性或组合唯一性(比如 created_at, id),否则可能漏行或重复。

常见错误现象:OFFSET 100000 LIMIT 20 执行秒级甚至超时;而等价的 keyset 查询毫秒级返回。

如何写一个安全的 keyset 分页 SQL

核心是把“第 N 页”转换成“大于上一页最后一条的排序键”。假设按 created_at DESC

, id DESC 排序,且每页取 20 条:

  • 第一页:直接查最大值,SELECT * FROM posts ORDER BY created_at DESC, id DESC LIMIT 20
  • 后续页:用上一页最后一条的 (created_at, id) 做条件,注意方向要和 ORDER BY 一致
  • 第二页示例(假设上一页最后一条是 ('2025-05-01 10:00:00', 12345)):
    SELECT * FROM posts WHERE (created_at, id)

关键点:

  • 多字段排序必须用行值比较(PostgreSQL/MySQL 8.0+ 支持),不能拆成两个独立条件(易出错)
  • 方向必须严格匹配:DESC 对应
  • 如果排序字段允许 NULL,需额外处理(例如加 IS NOT NULL

MySQL 和 PostgreSQL 的语法差异与陷阱

MySQL 5.7 不支持行值比较,得改写为复合条件:

等价于上面的 MySQL 5.7 写法:
WHERE created_at

PostgreSQL 支持标准行值语法,但要注意:

  • (a, b) 等价于 a ,语义一致
  • 如果字段类型是 TIMESTAMP WITH TIME ZONE,时区不一致会导致游标失效

常见坑:

  • 忘记给排序字段建联合索引(必须是 INDEX(created_at, id),顺序不能反)
  • 在 WHERE 中混用其他过滤条件却没包含在索引里,导致索引失效
  • 游标值被前端篡改或精度丢失(比如 JS 把时间截断成秒级)

如何生成和校验游标值(cursor)

游标不是随意拼的字符串,它是排序键的编码结果。推荐做法:

  • 服务端从查询结果中取最后一条的排序字段,JSON 序列化后 Base64 编码,例如:base64_encode(json_encode(['2025-05-01 10:00:00', 12345]))
  • 前端只传这个字符串,后端解码后直接用于 WHERE,避免类型解析错误
  • 务必校验解码后的数组长度和字段类型,防止注入或越界(如传入 ["abc", null]
  • 不要用主键 ID 单独做游标——如果业务允许 ID 不连续或有删改,极易漏数据

游标分页真正难的不是写 SQL,而是保证排序键稳定、索引有效、编码防篡改。一旦某条记录的 created_at 被更新,它就可能在下一页重复出现——这点容易被忽略。


# mysql  # js  # 前端  # json  # 编码  # 后端  # 为什么  # sql  # NULL  # select  # timestamp  # 字符串 


相关栏目: 【 网站优化151355 】 【 网络推广146373 】 【 网络技术251813 】 【 AI营销90571


相关推荐: JavaScript数据类型有哪些_如何准确判断一个变量的类型  Laravel如何优雅地处理服务层_在Laravel中使用Service层和Repository层  HTML5空格在Angular项目里怎么处理_Angular中空格的渲染问题【详解】  JavaScript常见的五种数组去重的方式  Laravel如何使用Telescope进行调试?(安装和使用教程)  米侠浏览器网页图片不显示怎么办 米侠图片加载修复  小视频制作网站有哪些,有什么看国内小视频的网站,求推荐?  如何确保西部建站助手FTP传输的安全性?  Laravel如何处理和验证JSON类型的数据库字段  深入理解Android中的xmlns:tools属性  Microsoft Edge如何解决网页加载问题 Edge浏览器加载问题修复  谷歌Google入口永久地址_Google搜索引擎官网首页永久入口  Edge浏览器提示“由你的组织管理”怎么解决_去除浏览器托管提示【修复】  javascript中闭包概念与用法深入理解  Win11怎么关闭资讯和兴趣_Windows11任务栏设置隐藏小组件  Firefox Developer Edition开发者版本入口  Laravel如何使用Guzzle调用外部接口_Laravel发起HTTP请求与JSON数据解析【详解】  Laravel与Inertia.js怎么结合_使用Laravel和Inertia构建现代单页应用  Laravel怎么实现支付功能_Laravel集成支付宝微信支付  做企业网站制作流程,企业网站制作基本流程有哪些?  网站页面设计需要考虑到这些问题  如何在VPS电脑上快速搭建网站?  JavaScript如何实现继承_有哪些常用方法  PHP 实现电台节目表的智能时间匹配与今日/明日轮播逻辑  网站制作公司哪里好做,成都网站制作公司哪家做得比较好,更正规?  Laravel怎么返回JSON格式数据_Laravel API资源Response响应格式化【技巧】  如何用AWS免费套餐快速搭建高效网站?  Laravel项目结构怎么组织_大型Laravel应用的最佳目录结构实践  如何在云虚拟主机上快速搭建个人网站?  Laravel怎么在Blade中安全地输出原始HTML内容  Android中AutoCompleteTextView自动提示  如何在阿里云服务器自主搭建网站?  魔毅自助建站系统:模板定制与SEO优化一键生成指南  北京网站制作公司哪家好一点,北京租房网站有哪些?  HTML5建模怎么导出为FBX格式_FBX格式兼容性及导出步骤【指南】  Laravel怎么实现验证码功能_Laravel集成验证码库防止机器人注册  微信小程序 wx.uploadFile无法上传解决办法  PHP正则匹配日期和时间(时间戳转换)的实例代码  安克发布新款氮化镓充电宝:体积缩小 30%,支持 200W 输出  Win11怎么修改DNS服务器 Win11设置DNS加速网络【指南】  HTML5空格和margin有啥区别_空格与外边距的使用场景【说明】  百度输入法ai面板怎么关 百度输入法ai面板隐藏技巧  网站制作报价单模板图片,小松挖机官方网站报价?  HTML5空格和nbsp有啥关系_nbsp的作用及使用场景【说明】  香港服务器网站推广:SEO优化与外贸独立站搭建策略  Laravel怎么使用Blade模板引擎_Laravel模板继承与Component组件复用【手册】  如何确认建站备案号应放置的具体位置?  香港服务器建站指南:外贸独立站搭建与跨境电商配置流程  Laravel如何使用Laravel Vite编译前端_Laravel10以上版本前端静态资源管理【教程】  Laravel如何为API生成Swagger或OpenAPI文档