当前位置: 首页 > news >正文

SQL深度分页问题案例实战

文章目录

  • 概述
    • 对比
    • 工作原理
    • 性能对比
      • 查询性能对比
      • 数据库负载对比
  • 代码示例
    • 传统分页示例
        • 请求
        • 响应
        • SQL执行
    • 游标分页示例
        • 首次请求(无游标)
        • 响应
        • 后续请求(使用游标)
        • SQL执行
    • 游标分页最佳实践
    • 总结
      • 选择建议

概述

对比

特性传统分页游标分页
定义使用 OFFSET 和 LIMIT 参数,通过跳过前面的记录来获取指定页的数据。使用一个游标(cursor)来标记当前位置,基于游标位置获取后续数据。
特点1. 需要知道总记录数(COUNT查询)
2. 使用页码(page)和每页数量(size)
3. 可以跳转到任意页面
1. 不需要总记录数
2. 使用游标(cursor)和每页数量(size)
3.只能顺序翻页,不能跳转
优点1. 可以跳转页面:用户可以直接跳转到第N页
2. 显示总数:可以显示总记录数和总页数
3. 实现简单:逻辑直观,易于理解
4. 兼容性好:所有数据库都支持OFFSETLIMIT
1.性能优秀:
+ 不需要COUNT(*)查询
+ 查询速度稳定,不受数据量影响
+ 使用索引高效定位
2.数据一致性:
+ 基于游标位置查询,不受数据变化影响
+ 不会出现重复或遗漏数据
3.资源消耗低:
+ 不需要统计总数
+ 查询效率高
缺点1.性能问题
+COUNT(*)查询在大数据量下很慢
+OFFSET越大,查询越慢(需要跳过更多记录)
2.数据一致性问题
+ 在翻页过程中,如果有数据新增或删除,可能导致:
- 重复数据(同一数据出现在两页)
- 遗漏数据(某些数据永远不会被看到)
1. 不能跳转页面:只能顺序翻页,不能直接跳转到第N页
2. 显示总数:无法显示总记录数和总页数
3. 实现复杂:需要处理游标编码/解码
4. 游标管理:需要确保游标的唯一性和稳定性
应用场景1.需要显示总数和总页数
+ 商品列表需要显示"共1000件商品"
+ 订单列表需要显示"共50页"
2.需要跳转页面
+ 用户可以输入页码跳转
+ 需要显示页码导航(1, 2, 3…)
3.数据量不大
+ 数据量在10万以内
+ 查询频率不高
4.管理后台
+ 管理员需要查看总数
+ 需要跳转到指定页面
1.大数据量场景
+ 数据量超过10万条
+ 需要高性能查询
2.移动端列表
+ 无限滚动加载
+ 不需要显示总数
3.实时性要求高
+ 数据频繁变化
+ 需要保证数据一致性
4.C端应用
+ 用户主要浏览最新数据
+ 不需要跳转到历史页面
5.时间线/动态流
+ 微博、朋友圈等时间线
+ 订单列表(按时间排序)

工作原理

-- 先查询总数SELECTCOUNT(*)FROMtrade_orderWHEREuser_id='xxx';-- 第一页(page=1, size=10)SELECT*FROMtrade_orderWHEREuser_id='xxx'ORDERBYcreate_timeDESCLIMIT10OFFSET0;-- 第二页(page=2, size=10)SELECT*FROMtrade_orderWHEREuser_id='xxx'ORDERBYcreate_timeDESCLIMIT10OFFSET10;

执行流程:

  1. 执行COUNT(*)查询获取总记录数
  2. 根据页码计算OFFSET = (page - 1) * size
  3. 执行主查询,跳过OFFSET条记录
  4. 返回当前页数据和总数
-- 第一页(无游标)SELECT*FROMtrade_orderWHEREuser_id='xxx'ORDERBYcreate_timeDESC,idDESCLIMIT11;-- 查询11条,用于判断是否有更多数据-- 第二页(使用游标)SELECT*FROMtrade_orderWHEREuser_id='xxx'AND(create_time<'2025-12-16 10:00:00'OR(create_time='2025-12-16 10:00:00'ANDid<'xxx-uuid'))ORDERBYcreate_timeDESC,idDESCLIMIT11;

执行流程:

  1. 如果有游标,解码游标获取createTimeid
  2. 添加游标条件:create_time < cursor.createTime OR (create_time = cursor.createTime AND id < cursor.id)
  3. 查询size + 1条数据(多查1条用于判断是否有更多数据)
  4. 如果返回size + 1条,说明还有更多数据,返回前size条并生成下一个游标
  5. 如果返回 ≤size条,说明没有更多数据

性能对比

查询性能对比

说明:

  • 传统分页的COUNT(*)查询时间随数据量线性增长
  • 传统分页的OFFSET越大,查询越慢
  • 游标分页性能稳定,不受数据量和页码影响

数据库负载对比

操作传统分页游标分页
每次查询SQL数量2条(COUNT + SELECT)1条(SELECT)
COUNT查询需要全表扫描或索引扫描不需要
OFFSET操作需要跳过N条记录不需要
索引利用部分利用完全利用

代码示例

传统分页示例

请求
POST /api-portal/trade/order/page { "page": 2, "size": 10, "status": 10, "createTime": ["2025-11-16 00:00:00", "2025-12-16 00:00:00"] }
响应
{ "code": 0, "data": { "list": [...], "total": 1000, "page": 2, "size": 10, "pages": 100 } }
SQL执行
-- 1. 查询总数 SELECT COUNT(*) FROM trade_order WHERE user_id = 'xxx' AND status = 10 AND create_time BETWEEN '2025-11-16' AND '2025-12-16'; -- 2. 查询数据 SELECT * FROM trade_order WHERE user_id = 'xxx' AND status = 10 AND create_time BETWEEN '2025-11-16' AND '2025-12-16' ORDER BY create_time DESC LIMIT 10 OFFSET 10;

游标分页示例

首次请求(无游标)
POST /api-portal/trade/order/cursor-page { "size": 10, "status": 10, "createTime": ["2025-11-16 00:00:00", "2025-12-16 00:00:00"] }
响应
{ "code": 0, "data": { "list": [...], "nextCursor": "MjAyNS0xMi0xNlQxMDowMDowMHw2ZTdhNTVlYi0zMzc0LTRjMDYtYmEzZi1mZGUwMmU5MGU5MWU=", "hasMore": true } }
后续请求(使用游标)
POST /api-portal/trade/order/cursor-page { "cursor": "MjAyNS0xMi0xNlQxMDowMDowMHw2ZTdhNTVlYi0zMzc0LTRjMDYtYmEzZi1mZGUwMmU5MGU5MWU=", "size": 10, "status": 10, "createTime": ["2025-11-16 00:00:00", "2025-12-16 00:00:00"] }
SQL执行
-- 查询 size + 1 条数据 SELECT * FROM trade_order WHERE user_id = 'xxx' AND status = 10 AND create_time BETWEEN '2025-11-16' AND '2025-12-16' AND (create_time < '2025-12-16 10:00:00' OR (create_time = '2025-12-16 10:00:00' AND id < 'xxx-uuid')) ORDER BY create_time DESC, id DESC LIMIT 11;

⚠️需要注意的问题:

  1. 游标设计
  • 游标必须唯一且稳定(使用createTime + id组合)
  • 游标字段必须有索引
  • 使用 Base64 编码保护游标
  1. 排序字段
  • 必须使用唯一字段作为排序依据(如id
  • 避免使用可能重复的字段(如createTime单独排序)
  1. 游标失效
  • 如果数据被删除,游标可能失效
  • 需要处理游标解析失败的情况
  1. 关键字查询
  • JOIN 查询时需要注意性能
  • 使用DISTINCT去重

游标分页最佳实践

推荐做法:

  1. 游标格式
// 使用 createTime|id 格式,Base64编码 cursor = Base64.encode("2025-12-16T10:00:00|uuid-string")
  1. 排序规则
ORDER BY create_time DESC, id DESC -- 确保排序的唯一性和稳定性
  1. 游标条件
WHERE (create_time < cursor.createTime OR (create_time = cursor.createTime AND id < cursor.id))
  1. 判断是否有更多数据
// 查询 size + 1 条 List<Order> orders = query(size + 1); boolean hasMore = orders.size() > size; if (hasMore) { orders = orders.subList(0, size); nextCursor = createCursor(orders.get(size - 1)); }

总结

选择建议

场景推荐方案原因
移动端列表(无限滚动)游标分页性能好,数据一致
管理后台(需要总数)传统分页需要显示总数和跳转
大数据量(>10万)游标分页性能优势明显
小数据量(<10万)传统分页实现简单
实时数据流游标分页数据一致性好
需要跳转页面传统分页游标分页不支持
http://www.cnnetsun.cn/news/88274.html

相关文章:

  • Git安装Windows版本并配置清华镜像用于TensorFlow贡献开发
  • Langchain-Chatchat 0.3.1 Windows本地部署指南
  • 私有云ACK:企业智能化转型的安全基座与算力引擎
  • Docker部署Qwen3-14B及GPU加速实战
  • SWIR相机
  • vLLM 0.11.0 发布:全面移除 V0 引擎,性能与多模态支持再升级
  • 从零开始:使用Git安装TensorRT及其依赖组件
  • 模块十八.集合
  • FLUX.1-dev服装生成LoRA模型体验
  • 使用nexus3搭建自己的制品服务器
  • 38、Linux 邮件与网页浏览实用指南
  • 41、互联网服务实用指南
  • LLaMA-Factory微调与模型中断续训实战
  • GitHub项目实践:Fork并定制你的个性化Anything-LLM前端界面
  • pythonstudy Day37
  • Linly-Talker结合RAG技术实现知识增强型虚拟客服系统
  • 用Deepseek-v3.1在Trae中编写AI中继程序
  • LobeChat能否实现思维导图输出?结构化内容展示尝试
  • 开源5G基站硬件参数
  • C#开发桌面应用调用GPT-SoVITS REST API实战
  • Dify Docker部署与使用全指南
  • 数组作为参数
  • 蜜罐技术-德迅猎鹰
  • Daily Report — Day 9 (Beta)
  • Seed-Coder-8B-Base与SonarQube智能集成路径
  • 基于CentOS7 DM8单机部署配置记录-20251216
  • 大模型入门:预训练、微调和蒸馏,一篇文章全掌握
  • LobeChat能否编写教案?教师备课自动化尝试
  • vLLM-Omni:全模态AI推理框架技术解析
  • 18、基于位置点的恢复