应改用键集分页,即基于分区键(如created_at)的游标条件过滤,避免OFFSET跨分区扫描;需确保游标字段与分区键一致,并配合覆盖索引和显式分区范围限定。

分区表上用 Offset 分页会更慢,不是因为 GORM,而是 MySQL/PostgreSQL 的执行逻辑没变
分区表本身不改变 OFFSET 的底层行为:数据库仍需扫描并丢弃前 N 行,只是扫描范围从“整张大表”变成“若干个分区”。但若分区键和排序字段不一致(比如按 created_at 分区,却按 id 排序),查询可能跨多个分区,实际扫描行数反而更多。常见错误现象是:同样 OFFSET 10000,在非分区表查 80ms,在分区表查 450ms——不是分页写错了,是分区没对齐查询模式。
关键点:
- MySQL 的 RANGE/LIST 分区在
WHERE条件未命中具体分区时,会触发全分区扫描;PostgreSQL 的 declarative partitioning 同理 - 如果分页用
ORDER BY id ASC,但表按created_at分区,数据库无法利用分区裁剪,仍要合并所有分区的有序结果再 offset - 复合主键
(id, created_at)+ 按created_at分区,且分页也用ORDER BY created_at DESC, id DESC,才可能让每个分区独立返回 top-K,再归并
GORM 游标分页在分区表上必须确保游标字段也是分区键
游标分页能绕过 OFFSET,但在分区表上失效的典型原因是:游标值(如 last_id 或 last_created_at)和分区键不匹配。例如按 created_at 分区,却用 id > ? 做游标条件——数据库无法定位到具体分区,仍要遍历全部分区找满足 id > 12345 的记录。
实操建议:
- 游标字段必须是分区键,或至少是分区键的前缀(如按
(year, month)分区,游标用created_at >= '2026-08-01') - 避免用
id当游标字段,除非表是按idRANGE 分区(少见,且需保证单调递增无空洞) - 首次请求加
WHERE created_at >= '2026-01-01'显式限定分区范围,防止优化器误判 - PostgreSQL 中可用
EXPLAIN (ANALYZE, BUFFERS)看是否只扫描目标分区;MySQL 用EXPLAIN PARTITIONS
Count 查询在分区表上极易误算总数,GORM 默认行为会放大问题
db.Model(&User{}).Count(&total) 在分区表上可能返回错误结果,尤其当表有 NULL 分区、默认分区(DEFAULT)或子分区时。GORM 的 Count 生成的是简单 SELECT COUNT(*) FROM table,但 MySQL 会为每个分区单独统计再汇总,而 PostgreSQL 可能因分区约束不完整导致漏计。
更危险的是链式调用:db.Where("status = ?", "active").Count(&total) —— 若 status 不在分区键中,且没有全局索引,COUNT 可能触发全分区扫描,耗时远超主查询。
安全做法:
- 对分区表,禁用
db.Model().Count(),改用手写子查询并显式指定分区范围:db.Raw("SELECT SUM(cnt) FROM (SELECT COUNT(*) AS cnt FROM users_202608 WHERE status = ? UNION ALL SELECT COUNT(*) FROM users_202607 WHERE status = ?) t", "active", "active").Scan(&total) - 若分区键可推导(如按月分区),用 Go 拼接确定的分区名,避免依赖优化器自动裁剪
- 缓存总数只适用于静态分区(如历史归档表),且需监听 DDL 变更事件清缓存
Preload 关联查询 + 分区表分页,GORM 容易触发 N+M 次跨分区扫描
在分区表上执行 db.Preload("Profile").Limit(20).Offset(10000).Find(&users),GORM 先查出 20 个用户(可能跨多个分区),再为这 20 个用户发一条 IN 查询加载 Profile——但如果 profiles 表也按用户 ID 分区,且分区规则不一致,这条 IN 查询可能扫遍所有 profile 分区。
这不是 GORM 的 bug,是分区设计与关联路径错配的结果。最常被忽略的一点:Preload 的 IN 查询无法利用分区裁剪,除非外键字段本身就是分区键。
规避方式:
- 禁止在分区表主查询中用
Preload,改用Joins+Select一次查出所需字段(前提是关联字段不多) - 若必须 Preload,确保关联表的分区键 = 主表的外键(如
profiles.user_id是其分区键),否则降级为应用层循环查(控制并发数) - 对高频关联场景,考虑冗余关键字段到主表(如把
profile.status同步到users表),用单表查询替代 JOIN


















