必须用游标分页替代OFFSET:因OFFSET会真实扫描并丢弃前N行,导致延迟线性增长、漏数据;应使用WHERE id > ? ORDER BY id LIMIT ?,配合主键索引和channel解耦拉取与处理。

直接结论:大批量数据分批处理不能依赖 OFFSET,必须用游标分页(cursor-based pagination)配合主键或时间戳条件推进;否则查到 10 万行后,每次查询实际扫描并丢弃前 N 行,Go 程序只是在为数据库的“无效读取”买单。
为什么 OFFSET 在 Go 里跑得越来越慢
MySQL/PostgreSQL 执行 SELECT * FROM logs ORDER BY id LIMIT 100 OFFSET 100000 时,并不会跳过前 10 万行——它会真实排序、逐行读取、再丢弃。Go 层反复调这个语句,database/sql 连接池和上下文超时(context.DeadlineExceeded)只是表象,根子在 SQL 执行计划本身。
- OFFSET 超过 10 万后,单次查询延迟常从 20ms 涨到 2s+,且随偏移线性增长
- 并发写入下容易漏数据:新插入记录可能挤在中间,导致某页重复或跳过
- SQLite 对大 OFFSET 几乎无优化;TiDB 会主动限流,直接报错
-
database/sql: statement expects 0 arguments, got 1常因参数绑定错位,但根源往往是 WHERE 条件没对齐游标逻辑
用主键 ID 实现稳定游标分页(最常用)
前提:表有单调递增、带索引的 id 字段(如自增主键),且业务能接受极小概率漏掉“恰好在分页间隙插入”的记录(通常可接受)。
- 核心是改写查询为
WHERE id > ? ORDER BY id LIMIT ?,每次把上一批最后一条的id当作下一页起点 - 必须确保
id字段有索引,否则WHERE id > ?会全表扫描 - 禁止用
ORDER BY id DESC配合id > ?,方向不一致会导致逻辑断裂 - 若主键是 UUID,改用复合游标:
WHERE (created_at, id) > (?, ?),索引需包含两个字段且顺序一致
示例代码片段:
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
立即学习“go语言免费学习笔记(深入)”;
rows, err := db.QueryContext(ctx, "SELECT id, user_id, content FROM events WHERE id > ? ORDER BY id LIMIT ?", lastID, batchSize)
if err != nil {
return err
}
defer rows.Close()
<p>for rows.Next() {
var e struct{ ID int64; UserID int; Content string }
if err := rows.Scan(&e.ID, &e.UserID, &e.Content); err != nil {
return err
}
processEvent(e)
lastID = e.ID // 记住本次最后 ID
}游标分页 + Go channel 解耦拉取与处理
光改 SQL 不够——真正卡住的是“处理慢导致堆积”。要把“从 DB 拉数据”和“干活”彻底解耦,避免 goroutine 泛滥或内存爆炸。
- 用一个 goroutine 持续游标拉取,把结果发进带缓冲的
chan []Item(如make(chan []Item, 10)) - 启动固定数量 worker(比如 4~8 个),从 channel 拿批次,处理完再发回结果或错误
- 每个 worker 用
defer func() { if r := recover(); r != nil { log.Printf("panic in worker: %v", r) } }()防止单条脏数据崩掉整个 goroutine - 失败批次别静默吞掉:记录原始
lastID、错误时间和堆栈,支持--retry-from-id=123456参数重入
容易被忽略的关键点
游标值不是随便传的字符串——它必须和数据库字段类型严格一致(比如 int64 别传成 int 或 string),否则 WHERE 条件失效,首尾页数据错乱;另外,前端传来的游标若为空(首次请求),SQL 要退化为无 WHERE 条件,而不是硬塞 WHERE id > 0 ——万一 id 从负数开始就全漏了。

















