mysql_page pagesize 如何实现游标分页?
这是一种基于排序字段值而不是偏移量的分页方法。
核心思想
记住上一页最后一条记录的位置,然后从这个位置开始查询下一页
逐步分解
第一页查询(基础查询)
SELECT * FROM loc_common_info
WHERE id NOT IN (1,2,3) -- 假设用户去过场地1,2,3
AND status = 'ACTIVE'
ORDER BY popularity_score DESC, id DESC
LIMIT 3; -- 假设每页3条
假设返回结果:
id | name | popularity_score |
---|---|---|
10 | 场地A | 95 |
8 | 场地B | 95 |
5 | 场地C | 90 |
关键点:我们记录最后一条记录的值:
lastScore = 90
(id=5的popularity_score)lastId = 5
(最后一条记录的id)
第二页查询(使用游标)
SELECT * FROM loc_common_info
WHERE id NOT IN (1,2,3)
AND status = 'ACTIVE'
-- 关键条件:找到排在"最后一条记录"之后的所有记录
AND (popularity_score < #{lastScore} OR (popularity_score = #{lastScore} AND id < #{lastId}))
ORDER BY popularity_score DESC, id DESC
LIMIT 3;
条件逻辑详解
条件分解:
AND (popularity_score < 90 OR (popularity_score = 90 AND id < 5)
)
这个条件的意思是:
-
popularity_score < 90
:- 找到所有评分严格小于90的记录
- 这些记录自然排在评分90的记录后面
-
popularity_score = 90 AND id < 5
:- 找到评分等于90,但id更小的记录
- 因为排序是
popularity_score DESC, id DESC
,所以id小的排在后面
实际数据示例
假设数据库中有这些数据:
id | popularity_score |
---|---|
10 | 95 |
8 | 95 |
5 | 90 |
3 | 90 |
7 | 85 |
2 | 85 |
4 | 80 |
第二页查询条件:(score < 90) OR (score = 90 AND id < 5)
符合条件的记录:
- id=3:满足
score = 90 AND id < 5
- id=7:满足
score < 90
- id=2:满足
score < 90
- id=4:满足
score < 90
排序后第二页结果:
id | popularity_score |
---|---|
3 | 90 |
7 | 85 |
2 | 85 |
为什么需要复合排序 ORDER BY score DESC, id DESC
?
如果只按 popularity_score DESC
排序:
问题场景:多个记录有相同的popularity_score
第一页:id=10(95), id=8(95), id=5(90) ← 最后一条id=5, score=90
第二页应该显示:id=3(90), id=7(85), id=2(85)
但如果只按score排序,数据库不保证相同score的记录顺序稳定,可能导致:
- 第一次查询:id=10(95), id=8(95), id=5(90)
- 第二次查询:id=10(95), id=8(95), id=3(90) ← 顺序变了!
复合排序确保:
- 主要按popularity_score降序
- score相同时,按id降序,保证顺序绝对稳定
前端-后端交互流程
// 第一页请求
PageRequest request1 = new PageRequest(0, 3, null); // 没有游标
PageResult result1 = service.getRecommendations(request1);// 返回结果包含下一页游标
String nextCursor = result1.getNextCursor(); // 编码为: "90_5"// 第二页请求
PageRequest request2 = new PageRequest(0, 3, "90_5"); // 使用游标
PageResult result2 = service.getRecommendations(request2);
与传统OFFSET分页对比
OFFSET分页(有问题):
-- 第一页
SELECT ... LIMIT 0, 3; -- 返回记录1,2,3-- 第二页
SELECT ... LIMIT 3, 3; -- 返回记录4,5,6
问题:如果第一页和第二页之间有新数据插入,会导致:
- 重复记录(新数据挤占了位置)
- 丢失记录(原有记录被挤到后面)
游标分页(稳定):
-- 第一页:返回id=10,8,5,记住lastScore=90, lastId=5
-- 第二页:查询score<90 OR (score=90 AND id<5)的记录
优势:
- 不受数据增删影响
- 性能更好(不需要计算OFFSET)
- 顺序绝对稳定
总结
游标分页的核心就是:记住上一页的终点,从终点开始找下一页,通过复合排序和精确的条件定位,确保分页的准确性和稳定性。
游标对象设计
@Data
public class RecommendationCursor {private String type; // "new", "rotate", "retain"private Double lastScore; // 上一页最后一条的popularity_scoreprivate Long lastId; // 上一页最后一条的IDprivate Integer page; // 当前页码private String randomSeed; // 随机种子public static RecommendationCursor parseCursor(String cursorStr, String type) {if (cursorStr == null) {return new RecommendationCursor(type, null, null, 0, generateRandomSeed());}// 解析游标逻辑...}public String toCursorString() {// 生成游标字符串逻辑...}
}
关键要点
- 不要使用 OFFSET:LIMIT offset, count 在数据量大时性能差且不稳定
- 游标分页:适合主内容区,保证稳定性和性能
- 随机分页:适合轮换区,增加多样性
- 复合排序:ORDER BY score DESC, id DESC 确保排序稳定
- 状态保持:通过游标或种子保持分页状态
这样设计后,每次分页查询都能返回不同的内容,同时保证性能和稳定性。