文章目录
- 概述
- 对比
- 工作原理
- 性能对比
- 查询性能对比
- 数据库负载对比
- 代码示例
- 传统分页示例
- 请求
- 响应
- SQL执行
- 游标分页示例
- 首次请求(无游标)
- 响应
- 后续请求(使用游标)
- SQL执行
- 游标分页最佳实践
- 总结
- 选择建议
概述
对比
| 特性 | 传统分页 | 游标分页 |
|---|---|---|
| 定义 | 使用 OFFSET 和 LIMIT 参数,通过跳过前面的记录来获取指定页的数据。 | 使用一个游标(cursor)来标记当前位置,基于游标位置获取后续数据。 |
| 特点 | 1. 需要知道总记录数(COUNT查询) 2. 使用页码(page)和每页数量(size) 3. 可以跳转到任意页面 | 1. 不需要总记录数 2. 使用游标(cursor)和每页数量(size) 3.只能顺序翻页,不能跳转 |
| 优点 | 1. 可以跳转页面:用户可以直接跳转到第N页 2. 显示总数:可以显示总记录数和总页数 3. 实现简单:逻辑直观,易于理解 4. 兼容性好:所有数据库都支持 OFFSET和LIMIT | 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;执行流程:
- 执行
COUNT(*)查询获取总记录数 - 根据页码计算
OFFSET = (page - 1) * size - 执行主查询,跳过
OFFSET条记录 - 返回当前页数据和总数
-- 第一页(无游标)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;执行流程:
- 如果有游标,解码游标获取
createTime和id - 添加游标条件:
create_time < cursor.createTime OR (create_time = cursor.createTime AND id < cursor.id) - 查询
size + 1条数据(多查1条用于判断是否有更多数据) - 如果返回
size + 1条,说明还有更多数据,返回前size条并生成下一个游标 - 如果返回 ≤
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;⚠️需要注意的问题:
- 游标设计
- 游标必须唯一且稳定(使用
createTime + id组合) - 游标字段必须有索引
- 使用 Base64 编码保护游标
- 排序字段
- 必须使用唯一字段作为排序依据(如
id) - 避免使用可能重复的字段(如
createTime单独排序)
- 游标失效
- 如果数据被删除,游标可能失效
- 需要处理游标解析失败的情况
- 关键字查询
- JOIN 查询时需要注意性能
- 使用
DISTINCT去重
游标分页最佳实践
✅推荐做法:
- 游标格式
// 使用 createTime|id 格式,Base64编码 cursor = Base64.encode("2025-12-16T10:00:00|uuid-string")- 排序规则
ORDER BY create_time DESC, id DESC -- 确保排序的唯一性和稳定性- 游标条件
WHERE (create_time < cursor.createTime OR (create_time = cursor.createTime AND id < cursor.id))- 判断是否有更多数据
// 查询 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万) | 传统分页 | 实现简单 |
| 实时数据流 | 游标分页 | 数据一致性好 |
| 需要跳转页面 | 传统分页 | 游标分页不支持 |