优先选游标分页;因排序字段重复时offset会漏数或重复,而游标(如id>last_id)依托排序字段+主键可确保顺序稳定,且避免深分页性能衰减。

分页选 limit/offset 还是游标?先看数据有没有“顺序稳定性”
如果排序字段可能重复(比如按 created_at 分页,但多条记录时间戳完全相同),offset 分页会漏数据或重复返回——因为数据库无法保证相同值之间的物理顺序稳定。游标分页(如用 id > last_id)天然规避这个问题,前提是排序字段+主键组合能唯一确定位置。
常见错误现象:offset=1000 时响应明显变慢,甚至超时;翻到后面几页,同一条记录在不同请求中忽隐忽现。
- 适合
limit/offset:后台管理类接口,数据量小(id)、不介意越往后越慢 - 适合游标分页:用户 Feed、订单列表、实时性要求高的场景;排序字段含时间/状态等易重复字段时必须用游标
- 别强行用
offset模拟游标(比如存offset值传给下一页),这既没解决稳定性问题,又保留了性能缺陷
用 SQLAlchemy 写游标分页,关键在 filter() 和 order_by() 的配合
游标分页不是“取第 N 条之后的 M 条”,而是“取排序值严格大于上一页最后一条的 M 条”。所以不能靠 offset,得靠 filter() 构造边界条件。
假设按 created_at DESC, id DESC 排序,上一页最后一条是 {"created_at": "2024-05-01 10:00:00", "id": 123},下一页查询应为:
立即学习“Python免费学习笔记(深入)”;
query = db.query(User).filter(
or_(
User.created_at < "2024-05-01 10:00:00",
and_(User.created_at == "2024-05-01 10:00:00", User.id < 123)
)
).order_by(User.created_at.desc(), User.id.desc()).limit(20)
注意点:
让 macOS 自动化像说话一样简单 —— 用代码控制你的 Mac,无需学习 AppleScript 系统要求 macOS 14+ (Sonoma) Python 3.9+ 需要开启:辅助功能 + 屏幕录制权限
-
order_by必须和游标构造逻辑严格一致,方向也要匹配(升序用>,降序用) - 复合游标必须覆盖所有
order_by字段,否则可能跳过数据 - 不要用
datetime字段直接比较(尤其带毫秒/时区),优先转成 UTC 时间戳或字符串标准化格式
前端传游标参数别拼 JSON,用固定字段名 + URL 编码
游标本质是服务端生成的不透明标记,但很多人误以为要前端解析/构造。正确做法:后端把游标编码成单个字符串(如 base64 或简单拼接),前端只负责透传。
错误示例:?cursor={"created_at":"2024-05-01T10:00:00Z","id":123} —— URL 中 JSON 易出编码问题,且暴露内部结构。
推荐方式:
- 后端生成游标:
cursor = base64.urlsafe_b64encode(json.dumps([dt_iso, id]).encode()).decode().strip('=') - 前端只传:
?cursor=abc123xyz,不做任何解析 - 后端收到后解码还原,再拆成排序字段值,用于
filter() - 游标里别塞敏感字段(如用户 email),哪怕已编码——游标可能被日志、监控系统捕获
MySQL 和 PostgreSQL 对游标分页的索引要求不一样
游标分页快不快,全看数据库能不能走索引。但两者的索引策略有差异:
- PostgreSQL:对
WHERE created_at 这种查询,复合索引 <code>(created_at, id)就够用 - MySQL(尤其是 8.0 以前):如果
ORDER BY和WHERE涉及不同字段方向(如WHERE created_at 但 <code>ORDER BY created_at DESC),可能无法高效利用索引。更稳妥的是建(created_at DESC, id DESC)显式降序索引(MySQL 8.0+ 支持) - 无论哪种数据库,都得用
EXPLAIN看执行计划,确认type是range或更好,而不是ALL
最容易被忽略的一点:游标分页的“下一页是否存在”,不能靠查 count(*) 判断——这会抵消掉游标的所有性能优势。正确做法是查 limit + 1 条,多出来那条只用来判断是否有下一页,不返回给前端。

















