必须用游标分页而非Limit+Offset——因聊天记录高并发写入、时间戳密集重复、需多表关联,Offset会导致漏消息或重复;应以created_at+id组合为游标,建联合索引,查limit+1条判断has_next。

客服系统的历史聊天记录分页,不能直接套用通用分页模板——数据高并发写入、用户实时翻页、时间戳密集重复、单次查询需关联多张表(如 chat_session、message、user),这些都会让 Limit+Offset 分页迅速失效。必须用游标分页,且排序字段组合要带索引保障稳定性。
为什么 Limit + Offset 在聊天记录里必然出错
聊天记录不是静态列表:新消息持续插入、用户可能撤回或标记已读、客服端和用户端同时发送。这时 Offset(1000) 不再对应“第 51 页”,而是某次快照下的物理偏移,下一次请求可能漏掉刚插入的两条消息,或重复返回上一页最后一条。
- MySQL 对
created_at字段做ORDER BY created_at DESC LIMIT 20 OFFSET 1000时,若 1000 行内存在 5 条同毫秒时间戳的消息,数据库返回顺序不固定,前端翻页就跳变 - 即使加了
ORDER BY created_at DESC, id DESC,如果id是自增但中间有删除,或使用 UUID,索引无法高效跳转,OFFSET 仍会扫描大量无用行 - 查总数
Count()没意义:用户关心的是“还能不能往下翻”,不是“总共有多少条”。强求精确总数反而拖慢响应,尤其当历史消息超百万条时
游标分页必须用 created_at + id 组合作为 cursor
前端首次请求不带参数,后端返回前 20 条,并把 messages[19].created_at 和 messages[19].id 拼成 cursor=2024-05-21T14:30:22Z_123456;下一次请求传 ?cursor=2024-05-21T14:30:22Z_123456,后端解析后生成 WHERE 条件。
- WHERE 条件必须是
WHERE (created_at, id) < (?, ?)(降序)或>(升序),不能拆成两个独立条件,否则索引失效 - 数据库联合索引必须按排序字段顺序建立:
INDEX idx_created_id (created_at, id),且方向一致(都 ASC 或都 DESC) - 解析
cursor时,用time.Parse(time.RFC3339, parts[0])而非time.Parse("2006-01-02 ..."),避免时区歧义 - 如果用户传的
cursor格式错误或记录不存在,直接返回空切片,不要 fallback 到Offset—— 那等于放弃游标一致性
关联查询 Preload 不能直接用在游标分页主查询里
聊天记录分页常需加载发送人昵称、头像、会话标题,但 Preload("Sender").Preload("Session") 会让 GORM 先查出 20 条 message,再发 N 条 IN 查询去批量加载关联数据——这看似合理,实则埋雷:
- Preload 的 IN 查询会加载所有匹配的关联记录,比如某条消息的 sender 关联了 50 个未读通知,全被拉下来
- 更严重的是,Preload 无法控制“每个会话只取最新 3 条消息”这类业务逻辑,它只做 1:N 映射
- 正确做法是两步走:第一步用游标查出
message.id列表(只查 ID 和必要字段);第二步用db.Where("id IN ?", ids).Preload(...)批量加载,且对每个 Preload 加Order和Limit(GORM 支持Preload("Orders", db.Order("created_at DESC").Limit(3)))
Count 查询必须放弃,改用 has_next 语义
客服系统中,“总共有 12876 条消息”对用户毫无价值;“还有下一页”才是关键。与其执行耗时的 COUNT(*),不如多查 1 条:
- 每次游标查询取
limit + 1条(例如要显示 20 条,实际查 21 条) - 如果结果长度 > 20,说明还有下一页,把第 21 条的
created_at和id作为 next_cursor;否则has_next = false - 这样省掉一次 COUNT 查询,响应稳定在 10–30ms,且完全规避了总数不准的问题(比如查总数时刚插入一条,分页查时又删了一条)
真正难的不是写 SQL,而是让游标值能跨服务、跨实例保持语义一致——比如 created_at 必须用数据库服务器时间(NOW()),不能用应用层 time.Now();id 必须全局唯一且单调,别用 MySQL 的 auto_increment 在分库分表下乱序。这些细节一漏,分页就变成随机抽样。


















