sqlite下gorm分页中,limit(0)返回全表、offset(-1)静默忽略、无mvcc导致结果易跳变,且必须显式order by rowid等唯一字段才能保证一致性。

SQLite 下用 GORM 分页,Limit 和 Offset 行为与 MySQL/PostgreSQL 有本质差异:它不 panic 负数 Offset,但 Limit(0) 会返回全表;且无 MVCC,高并发写入时分页结果更易“跳变”。别默认它和主流数据库表现一致。
SQLite 的 Limit/Offset 特殊行为
SQLite 对分页参数容忍度高,但这恰恰是陷阱源头:
-
Offset(-1)不 panic,而是静默忽略,导致第 1 页数据被意外跳过 -
Limit(0)在 SQLite 中等价于 “不限制”,返回全部记录 —— 这和 PostgreSQL 报错、MySQL 返回空完全不同 - 没有行级锁和 MVCC 快照,
Find执行期间若其他连接插入/删除,同一页可能漏或重复(比 MySQL 更明显) - 即使加了
Order("id ASC"),若 id 非连续(如删过记录),Offset计算出的“第 N 页”物理位置仍会漂移
为什么 SQLite 尤其不能省略 Order
SQLite 的查询优化器对无序 LIMIT 更激进,常直接走索引最左前缀并随机截断,导致结果不可重现:
- 执行两次
db.Limit(10).Offset(20).Find(&users),很可能返回不同集合,哪怕表无任何写入 -
ORDER BY rowid ASC比ORDER BY id ASC更稳 —— 因为rowid是 SQLite 内置、永不重复、自动递增的隐式主键 - 如果业务用了自定义
id(非 INTEGER PRIMARY KEY),必须确保它有唯一索引,否则排序无意义
Count 查询在 SQLite 中的性能盲区
SQLite 的 COUNT(*) 在无 WHERE 条件时极快(直接读 sqlite_master),但一旦带条件,尤其涉及 LIKE 或函数,就会触发全表扫描:
-
db.Where("name LIKE ?", "%foo%").Count(&total)在 10 万行表上可能耗时 300ms+,而主查询只要 5ms - SQLite 不支持子查询物化,
db.Raw("SELECT COUNT(*) FROM (SELECT 1 FROM users WHERE ... ) AS t")和直接Count()性能几乎一样 - 若前端只要“是否有下一页”,建议查
pageSize + 1条,用len(users) > pageSize判断,彻底绕开COUNT
游标分页在 SQLite 中反而更简单,但要注意字段选择
SQLite 没有 OFFSET 性能断崖,但游标分页仍是更可靠的选择,尤其在桌面或移动端本地数据库场景:
- 用
WHERE rowid > ?最稳妥 ——rowid原生索引、单调、不为空,无需额外建索引 - 避免用
created_at作游标字段:SQLite 的DATETIME默认精度仅秒级,高并发插入极易重复 - 首次请求可直接
db.Order("rowid ASC").Limit(21).Find(&users),取users[20].RowID作为下一页游标 - 注意:GORM v2 默认不映射
rowid,需在模型中显式声明RowID int64 `gorm:\"column:rowid;primaryKey\"`
SQLite 分页真正的复杂点不在语法,而在“一致性预期”——它不像服务端数据库那样提供事务隔离承诺。你写的 Offset 看似确定,实则依赖底层文件系统状态。上线前务必用真实数据量+并发写入压测,而不是只跑单线程单元测试。











