一、什么是游标分页?
游标分页是一种更高效的分页方式,它通过"记住当前位置"而不是"数页码"来实现分页。就像看书时夹书签一样,游标分页会记住你看到哪里了,下次直接从那里继续。
传统分页 vs 游标分页
传统分页(LIMIT-OFFSET):
- 工作原理:每次都要从头开始数
- 类似场景:老师点名,“从第21号同学开始,叫10个人”
游标分页:
- 工作原理:记住最后一条的位置
- 类似场景:看书时夹书签,“从第128页继续看”
二、为什么需要游标分页?
传统分页有三个大问题:
-
慢:越往后翻页越慢
- 查第1页:
LIMIT 10 OFFSET 0
→ 快 - 查第100页:
LIMIT 10 OFFSET 1000
→ 慢
- 查第1页:
-
不稳定:数据变化会导致重复或遗漏
- 新增数据:可能导致重复显示
- 删除数据:可能导致记录丢失
-
浪费资源:每次都要处理所有数据
三、游标分页怎么用?
基础用法(使用ID)
-- 第一页
SELECT * FROM products ORDER BY id DESC LIMIT 10;
-- 下一页(记住最后一条id=123)
SELECT * FROM products
WHERE id < 123 -- 关键在这里
ORDER BY id DESC
LIMIT 10;
高级用法(多字段排序)
-- 按价格排序,价格相同按ID排序
SELECT * FROM products
WHERE (price < 上一页最后价格)
OR (price = 上一页最后价格 AND id < 上一页最后ID)
ORDER BY price DESC, id DESC
LIMIT 10;
四、游标分页的三大优势
- 速度快:不管翻到第几页,速度都一样快
- 稳定性好:数据变化影响小
- 节省资源:数据库不用处理不需要的数据
五、实际应用中的问题与解决方案
问题1:删除数据导致页面变化
场景:
- 第一页:id 1-10
- 删除id=5
- 第二页本应从11开始,但现在id=11变成了第10条
解决方案:
// 记录已经看过的ID
Set<Long> viewedIds = new HashSet<>();
// 查询时排除已读
String sql = "SELECT * FROM products " +
"WHERE id < ? AND id NOT IN (" + viewedIds + ") " +
"ORDER BY id DESC LIMIT 10";
问题2:新增数据导致顺序变化
解决方案:
-- 添加时间字段辅助判断
SELECT * FROM products
WHERE (created_at < ?last_time OR
(created_at = ?last_time AND id < ?last_id))
ORDER BY created_at DESC, id DESC
LIMIT 10;
六、最佳实践建议
-
简单场景:只用ID游标
SELECT * FROM table WHERE id > ?last_id ORDER BY id LIMIT 10;
-
复杂排序:用组合游标
SELECT * FROM products WHERE (price < ? OR (price = ? AND id < ?)) ORDER BY price DESC, id DESC LIMIT 10;
-
高并发系统:缓存游标位置
# 用Redis记录每页的最后位置 cursor = redis.get(f"page:{page_num}")
七、不同数据库的实现
数据库 | 推荐写法 |
---|---|
MySQL | WHERE id < ?last_id |
PostgreSQL | WHERE (price,id) < (?,?) |
MongoDB | find({$or: [{price:{$lt:lastPrice}}, {price:lastPrice, _id:{$lt:lastId}}]}) |
八、常见问题解答
Q:游标分页能跳页吗?
A:原生不支持,但可以通过缓存页码到游标的映射关系实现
Q:数据经常变动怎么办?
A:使用"创建时间+ID"组合游标,或者接受最终一致性
Q:游标需要加密吗?
A:建议加密,防止用户篡改游标参数
总结
游标分页是处理大数据分页的最佳选择,它解决了传统分页的三个痛点:速度慢、不稳定、资源浪费。实现时要记住三个要点:
- 选择稳定的游标字段(如ID、时间戳)
- 确保排序规则明确(多字段组合排序)
- 合理设计索引(与ORDER BY保持一致)