Go:1.17.7
Gorm:gorm.io/gorm v1.22.3
Gorm-Sqlite3-driver: gorm.io/driver/sqlite v1.2.4
mattn-Sqlite3: github.com/mattn/go-sqlite3 v2.0.1+incompatible
Sqlite3:3.40.0
场景
使用gorm
CreateInBatches
功能,批量插入100个对象 A
,对象 A
有 17 个字段
// BatchCreate is 批量新增
func (a *A) BatchCreate(tx db.Client, batch []*A) error {
return tx.Table(a.TableName()).CreateInBatches(batch, len(batch)).Error
}
执行过后,报错:too many SQL variable
。
如果想直接知道解决方案的,可以往后跳,下面的一些细节是我的排查思路
排查
之前碰到 too many SQL variable
的错误,是因为 gorm.where(“a=? and b=?”, a,b,c) 。类似这样子,原本需要 2 个参数就可以,但是传了 3 个的情况,会报 too many SQL variable
。
顺着这个思路,就开始排查是不是 CreateInBatches
的参数有问题,导致了 SQL
执行出错。
不过,很遗憾,拿着打印的 SQL
去执行,发现没有报错,能直接批量写入 100 条数据,holy shit。那只能去问度娘了。
搜:
- gorm 批量写入
too many SQL variable
- gorm CreateInBatches 的坑
- gorm 批量写入上限
- …
找了很多,发现都没找到点上,或者根本不想关,那只能回归源码。
too many SQL variable
会是谁的报错?
- gorm
- sqlite-driver
- mattn-sqlite
- sdk/database
我们知道 go
访问数据库,都是通过驱动去访问,最后执行的结果也还是数据库返回的,gorm 这一层只是封装了一下调用方式,让我们更好用。所以,焦点就回归到,是不是 sqlite3 数据库本身对批量写入的 sql 语句有限制?顺藤摸瓜,找到了答案。
https://sqlite.org/limits.html#max_variable_number
这个讲的是 sqlite3 parameter 最大限制数量,也就是在 SQL 语句中的占位符 ?
(还有别的)。文中说,因为怕 SQL 语句里的 parameter 太多,例如到 1000000000 个,那会需要用到 G 为单位的存储,那会是灾难。为了防止分配如此巨大的内存空间,SQL 用 SQLITE_MAX_VARIABLE_NUMBER
这个配置来限制。重点来了:
- 版本在 2020-05-22 号前的 3.32.0 默认 999
- 版本在 3.32.0 后的默认 32766
验证一
查到这个信息后,当然要验证一下。验证的方式就是调整每次批量写入的数量,999 / 17 = 58.7(999 个 parameter,对象 A insert 需要 17 个 value),所有就测试了一下 58 和 59 分别的执行情况。
- 58 :success
- 59 :
too many SQL variable
解决方案一
调整每次 CreateInBatches 的 batchSize,改为 50,合适的范围。
// 最后调整成,最多 50
func (a *A) BatchCreate(tx db.Client, batch []*A) error {
return tx.Table(a.TableName()).CreateInBatches(batch, 50).Error
}
接着排查
可能你看到这里会有点奇怪,为什么解决了还要接着排查。不知道,你注意到没有,上线写着的是 3.32.0 前后有区别,我的 Sqlite3 版本是 3.40.0,那正常来说应该可以批量写入的呀,这是为什么呢?
Sqlite3 版本没问题,那会不会是 gorm sqlite 相关的包,版本兼容问题呢?
去看了 github 上 gorm、sqlite3-driver、mattn-sqlite3 的首页,发现都有新的版本。
因为我用的 gorm 和 sqlite3-driver 包比较旧,就改成了:
Gorm: gorm.io/gorm v1.24.2
Gorm-Sqlite3-driver: : gorm.io/driver/sqlite v1.4.3
mattn-Sqlite3: github.com/mattn/go-sqlite3 v1.14.15
因为这个时候,我是用一个新的 project 测试,不是在原有的项目上面。
验证二
切换完版本,根本不用改 batchSize,一条过,OMG
解决方案二
更新到最新版本的 gorm,自动会更新 sqlite3-driver 的包
验证三
如果是最新版本问题,那么是不是在 gorm 中间版本的时候,说明了某个版本只能兼容到 sqlite3 3.32.0。于是我就翻了 gorm 的 release 发现 v1.3.2 版本写着 update sqlite3 driver
。以为我发现了新大陆,被我找到了。
于是我把 gorm 版本改成了 v1.3.2,发现确实可以。正当我以为解决完问题的时候,我在我新的项目下改成 v1.2.4,也就是开头的版本,验证一下,就是因为版本太旧,不能兼容 Sqlite3 3.32.0 之后的版本的时候。我去,它竟然也可以。
这是为什么?这是为什么?比对了一下 go mod,看下两边的包版本是不是哪里不一样。最终发现罪魁祸首:
mattn-Sqlite3: github.com/mattn/go-sqlite3 v2.0.1+incompatible
mattn-Sqlite3: github.com/mattn/go-sqlite3 v1.14.15
就是因为这两个包的不同。
验证四
旧项目不能更新到最新版本的 gorm,所以就用 replace 把包替换一下,很真的可以
解决方案三
多看看这些包的官方文档,在 https://github.com/mattn/go-sqlite3 写着:
NOTE: The increase to v2 was an accident. There were no major changes or features.
明确地写着 v2 不能用呀~~
引申问题
go mod,为什么会去拉取到 v2 的包,我还没有分析出来
总结
虽然兜兜转转,最后竟然是因为包的缘故,导致了 too many SQL variable
,不过还是学到了很多东西。也让我注意了批量写入时,是需要注意批量写入的大小的。也不是看到默认值是 32766,那就意味着我可以一次性干到最大,这个想法是不对的。在 https://github.com/mattn/go-sqlite3/issues/704 这个 issue 有提到类似的问题,最下面有一个写法,是用 Prepare ,然后每次都批量写入一些,然后完成你最后需要批量写入的数据。再看了下 gorm 的 CreateInBatches,是一样的效果。
解决方案
- 限制批量写入的数量,太大性能可能就会下降
- 删掉旧的 go mod,拉取最新的包,在够用的前提下,不见得最新就是最好的
- 多学习,多看源码
启发文章
https://blog.csdn.net/lovelyelfpop/article/details/51064664