用 pgx.Batch 替代循环 Exec 可将10万行INSERT从数分钟降至1–3秒,因其减少事务开销和round-trip次数;每批1000–5000行,需遍历batchResults捕获错误。

用 pgx.Batch 替代循环 Exec 是最直接有效的提速方式
单条 INSERT 在 10 万行数据下可能耗时数分钟,而批量提交通常能压到 1–3 秒。根本原因不是网络延迟,而是 PostgreSQL 的事务开销和 Go 驱动的 round-trip 次数。pgx 的 Batch 把多条语句打包成一个请求发给服务端,避免了反复解析、计划、锁表等重复动作。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 不要手动拼接 SQL 字符串做“伪批量”,容易 SQL 注入且 pgx 不会优化执行计划
- 每批大小控制在 1000–5000 行之间:太小起不到合并效果;太大可能触发服务端内存压力或超时(尤其字段多或含
TEXT) - 显式调用
batchResults := batch.Do(ctx)后必须遍历batchResults,否则错误会被静默吞掉(比如某一行主键冲突,但你不读结果就发现不了) - 示例关键片段:
batch := &pgx.Batch{} for _, u := range users { batch.Queue("INSERT INTO users(name, email) VALUES($1, $2)", u.Name, u.Email) } br := conn.SendBatch(ctx, batch) defer br.Close() for i := 0; i < len(users); i++ { _, err := br.Exec() if err != nil { // 这里才能捕获第 i 条失败的具体错误 } }
用 COPY FROM STDIN 处理纯插入、无业务逻辑的百万级场景
当数据源是文件或内存切片,且不需要触发 INSERT 触发器、不依赖返回值、也不需要单条失败隔离时,COPY 是 PostgreSQL 原生最快的写入路径,比 Batch 还快 3–5 倍。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
-
pgx.Conn.CopyFrom要求数据已转为[]interface{}切片,注意类型对齐:数据库列顺序、Go 类型(如*string不能传string) - 遇到
ERROR: invalid input syntax for type timestamp多半是时间字段没转成time.Time或为nil却用了非指针类型 - 不要在事务外调用
CopyFrom—— 它本身不自动开启事务,但失败时不会自动回滚,得自己包一层BeginTx - 示例:
rows := make([][]interface{}, len(users)) for i, u := range users { rows[i] = []interface{}{u.Name, u.Email, u.CreatedAt} // 顺序必须和表定义一致 } _, err := conn.CopyFrom(ctx, pgx.Identifier{"users"}, []string{"name", "email", "created_at"}, pgx.CopyFromRows(rows), )
别在批量插入里混用 RETURNING 或复杂表达式
加 RETURNING id 看似方便,但会让 PostgreSQL 放弃批量执行优化路径,退化成逐行处理。实测 10 万行插入 + RETURNING 比不加慢 8–10 倍,且内存占用翻倍。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 真需要插入后 ID,优先用序列预分配(
SELECT nextval('users_id_seq') FROM generate_series(1,10000))再插入,或改用INSERT ... SELECT批量生成 - 如果必须用
RETURNING,只在小批量(≤100 行)且业务强依赖返回值时启用 - 避免在
INSERT里写now()、uuid_generate_v4()等函数 —— 它们会在每行重复执行,拖慢整体速度;改用客户端生成后传入
连接池和上下文超时设置不当会掩盖真实瓶颈
常见现象:批量插入中途卡住几秒后报 context deadline exceeded,但 CPU 和 DB 负载都正常。问题往往不在 SQL,而在连接池被占满或上下文提前取消。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- pgx 连接池默认
MaxConns: 4,批量插入时建议设为MaxConns: 20–50(根据 DB 实际并发能力调整),否则大量Batch请求排队等待连接 - 不要复用短生命周期的
context.WithTimeout(ctx, time.Second)做批量操作 —— 插入 10 万行本就要几秒,超时应按任务粒度设(如5 * time.Minute) - 用
pgxpool.Config.MinConns预热连接池,避免首次批量时频繁建连抖动 - 监控
pg_stat_activity中状态为idle in transaction的连接,那是Batch或CopyFrom出错后没正确关闭导致的泄漏
批量插入真正的复杂点不在语法,而在于错误传播路径变长、资源边界更敏感、以及数据库侧的隐式行为(比如 WAL 写放大、索引维护成本)。哪怕代码看着跑通了,也得查 pg_log 确认没触发 auto-vacuum 或 checkpoint stall。











