游标分页(Cursor-based Pagination)是一种针对大数据量分页查询的高效解决方案,尤其适用于避免深分页性能问题的场景(如千万级数据分页)。以下是其核心原理、实现方式及优化细节:
一、传统分页的问题
传统分页使用 LIMIT offset, size(如 LIMIT 10000, 50)时:
- 性能瓶颈:MySQL 需要先扫描前 offset + size 条数据,再丢弃前 offset 条,效率极低。
- 数据漂移:若数据频繁增删,OFFSET 分页会导致重复或遗漏(如第2页查询时,第1页数据已变化)。
二、游标分页的核心思想
基于有序游标:使用 唯一且有序的字段(如自增主键、时间戳)作为分页锚点。
定位式查询:每次查询基于上一页的最后一个值,直接跳过已读数据。
三、实现步骤
1. 确定游标字段
理想字段:自增主键(id)、时间戳(created_at)等。
要求:字段唯一、有序,且数据按此字段排序。
2. 分页查询SQL
-- 第一页(初始查询)
SELECT id, phone, name
FROM students
ORDER BY id ASC
LIMIT 50;
-- 后续页(基于上一页的最后一个id)
SELECT id, phone, name
FROM students
WHERE id > {last_id} -- 锚点定位
ORDER BY id ASC
LIMIT 50;
- 分页参数传递
客户端:记录当前页的最后一个 id(即游标),将其作为下一页的查询条件。
服务端:无需计算总页数,只需返回数据及是否有下一页的标记。
四:代码示例
使用mybastis
- 定义 Mapper 接口
SELECT id, name, phone FROM students WHERE id > ? ORDER BY id LIMIT 50;
- 编写 XML 映射文件
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.mapper.StudentMapper">
<select id="selectStudentsByCursor" resultType="com.example.model.Student">
SELECT id, name, phone
FROM students
<if test="lastId != null and lastId != ''">
WHERE id > #{lastId}
</if>
ORDER BY id
LIMIT 50
</select>
</mapper>
- 在 Service 层调用 Mapper 方法
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class StudentService {
@Autowired
private StudentMapper studentMapper;
public void processStudents() {
String lastId = null; // 初始值为null,表示从头开始
while (true) {
List<Student> students = studentMapper.selectStudentsByCursor(lastId);
if (students.isEmpty()) {
break;
}
for (Student student : students) {
// 处理当前批次的数据,例如发送到Kafka
System.out.println("Processing student: " + student);
}
// 更新lastId为当前批次最后一个学生的id
lastId = students.get(students.size() - 1).getId();
}
}
}