不能用offset/limit做api分页,因其需全扫描前n行导致性能差,且并发增删时会漏数或重复;游标分页基于上一页末尾排序值(如id或(created_at,id))续查,避免扫描、保证一致性。

为什么不能用 offset/limit 做 API 分页
因为 OFFSET 在大数据量下会拖慢查询,比如 OFFSET 1000000 仍要扫描前一百万行;更严重的是,若中间有数据插入或删除,page=11 可能漏掉或重复返回记录。游标分页(cursor-based pagination)靠排序字段+上一页末尾值来定位下一页,跳过扫描、保证一致性。
如何构造游标值并嵌入 SQL 查询
游标本质是排序字段的编码值(如 id 或 created_at),必须满足:唯一、有序、不可变。推荐用 id(主键)或组合 (created_at, id) 防止时间重复:
- 前端传
cursor=12345表示“取 id > 12345 的前 N 条” - 后端需校验
cursor是合法整数或 base64 解码后的字符串(如base64_encode("2024-05-01 10:00:00|123")) - SQL 必须带
ORDER BY id ASC(方向需与游标逻辑一致),且WHERE id > ?不能写成>= - 查询时加
LIMIT N+1:多查 1 条用于判断是否有下一页,但只返回 N 条给客户端
示例片段:
$cursor = $_GET['cursor'] ?? null;
$limit = min((int)($_GET['limit'] ?? 20), 100);
$params = [];
$sql = "SELECT id, title, created_at FROM posts WHERE 1=1";
if ($cursor !== null) {
$sql .= " AND id > ?";
$params[] = (int)$cursor;
}
$sql .= " ORDER BY id ASC LIMIT " . ($limit + 1);
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
$rows = $stmt->fetchAll();
$hasNext = count($rows) > $limit;
if ($hasNext) {
array_pop($rows); // 剔除多查的那条
}
$nextCursor = $hasNext ? (string)$rows[count($rows)-1]['id'] : null;
如何生成和校验安全的复合游标(含时间戳)
仅用 id 不够灵活,尤其当业务按时间排序时。复合游标如 "2024-05-01 10:00:00|123" 能兼顾顺序与唯一性,但需注意:
- 时间字段必须带毫秒或用
datetime(3)类型,否则同秒内多条记录会撞游标 - 拼接前对时间做标准化:统一时区(如 UTC)、固定格式
date('Y-m-d H:i:s.u'),再截断微秒位数保持可比性 - 游标值必须 URL-safe,建议用
base64_encode()编码,解码失败即拒绝请求 - WHERE 条件要拆开写:
WHERE (created_at, id) > (?, ?)(MySQL 支持元组比较),而不是字符串拼接比较
错误示范:WHERE CONCAT(created_at, '|', id) > ? —— 无法走索引,性能归零。
客户端怎么解析 next_url 并避免无限循环
API 响应里不应返回 page 数,而应提供 next_cursor 字段或完整 next_url:
- 推荐直接返回
"next": "/api/posts?limit=20&cursor=MTIzNDU=",由后端生成,客户端只负责调用 - 游标值必须是 opaque token(不暴露内部结构),哪怕只是 base64 编码的整数,也别让前端尝试解析或递增
- 首次请求不带
cursor,但后端需明确处理该情况(如设WHERE id > 0或空条件) - 若用户反复请求同一游标,应返回相同结果(幂等),但数据库无变更时,结果自然一致
容易被忽略的一点:游标分页无法跳转到任意页(比如“最后一页”),这是设计使然——如果你的需求真需要随机跳页,说明游标分页并不适合当前场景。
大量免费API接口:立即使用
涵盖生活服务API、金融科技API、企业工商API、等相关的API接口服务。免费API接口可安全、合规地连接上下游,为数据API应用能力赋能!











