GORM本身不支持分库分表分页,必须手动实现分片路由+游标分页;Limit/Offset在分库分表下失效,因各分片数据孤立、全局排序与总数无法保障,唯一可行方案是带分片键的游标分页。

GORM 本身不支持分布式数据库的分库分表,所谓“GORM 分库分表分页”本质是应用层手动路由 + 单库分页逻辑的组合,不能依赖 GORM 自动识别分片键或合并结果。
分库分表后,GORM 的 Limit/Offset 分页直接失效
分库分表(如按 user_id % 4 拆到 4 个库)后,每张物理表只存部分数据。此时若仍用 db.Offset(1000).Limit(20):
- 查的是单个分片的第 1001–1020 条,不是全局排序后的结果 —— 总数不准、顺序错乱、翻页跳变
- 无法跨库聚合排序:GORM 不会帮你把 4 个库的结果拉回来 merge sort
- Count(*) 只统计当前分片,总数永远是 1/4,前端页码全崩
- 即使加了
Order("created_at DESC, id DESC"),各分片内部有序,但全局无序
游标分页是唯一可行的分库分表分页方案
必须放弃“页码”概念,改用“游标+分片路由”双控制。核心是:前端传 cursor(如 base64 编码的 "1678923456789|10023"),后端解出时间戳和 ID,再决定查哪个库、哪张表、发什么 SQL。
- 游标值必须包含分片键信息(如
user_id)或能推导出分片键(如关联订单查用户,用order.user_id路由) - 首次请求不带 cursor,需指定默认分片(如查
users_0表),返回结果时附带本分片最后一条的created_at和id - 后续请求解析 cursor → 算出目标分片 → 在该分片执行
WHERE (created_at, id) > (?, ?) ORDER BY created_at DESC, id DESC LIMIT 21 - 若某分片无新数据,需主动查下一个分片(最多遍历全部分片),但必须限制总查询次数(如 ≤ 3),避免雪崩
- 前端拼游标时,必须用
base64.RawURLEncoding.EncodeToString,避免+或/被 URL 解析破坏
Count 总数在分库分表下应彻底放弃
跨 N 个库执行 COUNT(*) 并求和,性能差、一致性低、还容易因事务未提交导致偏差。真实业务中,90% 场景不需要精确总数。
- UI 只显示“下一页”,就只查
limit + 1条,有第limit + 1条则返回has_next: true,否则false - 若必须显示“共 XXX 条”,只能走异步预计算(如 Flink 统计每日 UV)或近似估算(HyperLogLog),别在接口里实时扫
- 绝对不要写
db.Unscoped().Count()或db.Session(&gorm.Session{NewDB: true}).Model(&User{}).Count()—— 它们只扫一个分片 - 缓存总数只适用于静态配置类数据(如字典表),用户行为类数据缓存反而引入严重一致性风险
分片键设计直接影响分页能否落地
分片键选错,游标分页根本没法做。例如用 uuid 作分片键,就无法基于时间或 ID 做范围查询;用 create_time 分片,又会导致热点写入。
- 首选单调递增整型 ID(如 Snowflake)作为分片键,既能路由,又能用于游标比较
- 次选复合键:
(shard_key, created_at),索引建为INDEX idx_shard_created (shard_key, created_at DESC, id DESC) - 禁止用哈希后字符串(如 MD5(user_email))作分片键 —— 游标无法构造 WHERE 条件
- 所有分页查询必须带上分片键条件(如
WHERE user_id IN (1001, 1002, ...)),否则变成广播查询,O(N) 扫描
分库分表不是加个中间件就能自动分页的事。GORM 只管单库 SQL 拼装,分片路由、游标生成、跨库归并、错误熔断,都得自己写死逻辑。最容易被忽略的点是:没在游标里嵌入分片标识,导致后续请求路由到错误分片,查到空结果还误以为到底了。


















