文章目录
概述
对比
| 特性 | 传统分页 | 游标分页 |
|---|---|---|
| 定义 | 使用 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. 时间线/动态流 + 微博、朋友圈等时间线 + 订单列表(按时间排序) |
工作原理
sql
-- 先查询总数
SELECT COUNT(*) FROM trade_order WHERE user_id = 'xxx';
-- 第一页(page=1, size=10)
SELECT * FROM trade_order
WHERE user_id = 'xxx'
ORDER BY create_time DESC
LIMIT 10 OFFSET 0;
-- 第二页(page=2, size=10)
SELECT * FROM trade_order
WHERE user_id = 'xxx'
ORDER BY create_time DESC
LIMIT 10 OFFSET 10;
执行流程:
- 执行
COUNT(*)查询获取总记录数 - 根据页码计算
OFFSET = (page - 1) * size - 执行主查询,跳过
OFFSET条记录 - 返回当前页数据和总数
sql
-- 第一页(无游标)
SELECT * FROM trade_order
WHERE user_id = 'xxx'
ORDER BY create_time DESC, id DESC
LIMIT 11; -- 查询11条,用于判断是否有更多数据
-- 第二页(使用游标)
SELECT * FROM trade_order
WHERE user_id = 'xxx'
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 - 添加游标条件:
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条记录 | 不需要 |
| 索引利用 | 部分利用 | 完全利用 |
代码示例
传统分页示例
请求
plain
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"]
}
响应
plain
{
"code": 0,
"data": {
"list": [...],
"total": 1000,
"page": 2,
"size": 10,
"pages": 100
}
}
SQL执行
plain
-- 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;
游标分页示例
首次请求(无游标)
plain
POST /api-portal/trade/order/cursor-page
{
"size": 10,
"status": 10,
"createTime": ["2025-11-16 00:00:00", "2025-12-16 00:00:00"]
}
响应
plain
{
"code": 0,
"data": {
"list": [...],
"nextCursor": "MjAyNS0xMi0xNlQxMDowMDowMHw2ZTdhNTVlYi0zMzc0LTRjMDYtYmEzZi1mZGUwMmU5MGU5MWU=",
"hasMore": true
}
}
后续请求(使用游标)
plain
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执行
plain
-- 查询 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去重
游标分页最佳实践
✅ 推荐做法:
- 游标格式
plain
// 使用 createTime|id 格式,Base64编码
cursor = Base64.encode("2025-12-16T10:00:00|uuid-string")
- 排序规则
plain
ORDER BY create_time DESC, id DESC
-- 确保排序的唯一性和稳定性
- 游标条件
plain
WHERE (create_time < cursor.createTime
OR (create_time = cursor.createTime AND id < cursor.id))
- 判断是否有更多数据
plain
// 查询 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万) | 传统分页 | 实现简单 |
| 实时数据流 | 游标分页 | 数据一致性好 |
| 需要跳转页面 | 传统分页 | 游标分页不支持 |