用sql.db批量插入应避免循环exec,首选拼多值insert(每批100–500行)或数据库原生接口(如postgresql的copy、mysql的load data);必须用prepare复用语句、开启事务包裹整批,并安全处理参数与null。

Go 里用 sql.DB 做批量插入,别直接循环 Exec
单条 INSERT 循环执行在 Go 里很常见,但数据量一过几百行,性能就断崖式下跌——网络往返、事务开销、SQL 解析全被放大。真正有效的批量插入不是“多插几次”,而是让一次 SQL 尽可能塞进更多数据,或复用连接与预编译语句。
核心路径只有两条:INSERT INTO ... VALUES (...), (...), (...) 拼多值语句,或用数据库原生批量接口(如 PostgreSQL 的 COPY、MySQL 的 LOAD DATA INFILE)。前者通用,后者快但受限于权限和文件路径。
- 拼多值时,注意单次语句长度限制:MySQL 默认
max_allowed_packet=4MB,PostgreSQL 默认statement_timeout和内存限制也会影响;建议每批 100–500 行,视字段数和长度动态调整 - 必须用
Prepare+Exec复用 stmt,避免每次解析 SQL;不要用db.Query或db.Exec直接拼字符串,有注入风险且无法复用 - 开启事务包裹整批插入,否则每条
INSERT都是独立事务,写 WAL/redo log 开销翻倍
拼接多值 INSERT 时如何安全处理参数和 NULL
手拼 VALUES (?, ?), (?, ?) 看似简单,但 nil、时间、字节切片等类型容易出错,且不同驱动对 NULL 的占位符行为不一致(比如 pgx 要用 $1,mysql 用 ?)。
正确做法是:先确定参数总数,动态生成占位符串,再把所有值平铺进 []interface{} 切片传入 stmt.Exec。不要试图用 fmt.Sprintf 拼 SQL 字符串来替代参数绑定。
- 示例:插入 3 行,每行 2 列 → 占位符为
(?, ?), (?, ?), (?, ?),参数切片长度必须是3 * 2 = 6 -
nil值直接传nil,驱动会自动转成 SQLNULL;不要传空字符串或零值代替 - 时间类型统一用
time.Time,别转字符串;[]byte直接传,别 base64 编码 - 如果某列是 JSON 类型(如 PostgreSQL
JSONB),确保传入的是json.RawMessage或已序列化的[]byte,而非结构体指针
用 pgx 批量导入 PostgreSQL 的 COPY 接口
当数据源是内存切片或文件流,且目标库是 PostgreSQL 时,pgx.Batch 或底层 CopyFrom 比多值 INSERT 快 5–10 倍,因为它绕过 SQL 解析,直接走二进制协议传输。
注意:CopyFrom 要求数据结构与表字段严格对齐(顺序、类型),且不能触发触发器或默认值计算(除非显式指定 DEFAULT 占位)。
- 使用
conn.CopyFrom(ctx, pgx.Identifier{"table_name"}, []string{"col1", "col2"}, dataRows),其中dataRows是[][]interface{},每行对应一行数据 - 字段名必须小写(PostgreSQL 默认折叠为小写),且不能含双引号或特殊字符;若表名含 schema,用
pgx.Identifier{"schema", "table"} - 遇到
NULL,对应位置填nil;时间字段填time.Time;JSON 字段填json.RawMessage - 错误不会中断整个批次,
CopyFrom返回首个错误,需检查返回的int64插入行数判断是否全量成功
MySQL 批量插入没 COPY 怎么办?LOAD DATA LOCAL INFILE 不实用
MySQL 官方驱动不支持 LOAD DATA LOCAL INFILE(因安全限制,默认关闭且需服务端显式启用 local_infile=ON),生产环境基本不可用。实际能用的只有多值 INSERT + 事务 + Prepare,再加一点优化技巧。
关键点在于控制批大小和减少 GC 压力:避免每次批量都 new 大切片,复用 [][]interface{} 底层数组;字符串字段尽量用 unsafe.String(Go 1.20+)避免拷贝;时间字段提前转成 int64 存 Unix 时间戳(如果业务允许)。
- 设置
SET autocommit = 0后手动COMMIT,比依赖sql.Tx更轻量,尤其大批量时可减少 Tx 对象分配 - 用
mysql.ParseDSN显式加上parseTime=true&loc=Local,避免 time 解析开销 - 如果主键冲突频繁(如 UPSERT 场景),优先用
INSERT ... ON DUPLICATE KEY UPDATE,而不是先SELECT再决定插或更——网络 IO 更贵 - 监控
SHOW STATUS LIKE 'Com_insert%'和Slow_queries,确认是否真达到批量效果,还是被隐式拆成了单条
批量导入真正的瓶颈往往不在 Go 代码怎么写,而在数据库连接池配置、事务隔离级别、索引数量和磁盘 I/O。哪怕 SQL 写得再高效,如果 db.SetMaxOpenConns(5) 却并发跑 50 个批量任务,结果只是排队等连接。上线前务必用真实数据压测,看慢日志里到底卡在哪一步。











