先说我的结论:Prepare这个函数,在单次或低频查询里基本没什么收益,甚至可能更慢。但事情并非绝对——当同一SQL模板被重复执行数百次以上时,它的优势才会开始显现。当然,这有一个重要前提:数据库驱动必须支持真正的服务端预处理,比如MySQL版本≥5.7,且驱动配置正确。相比之下,sqlite3或pgx(非pool模式)的行为差异就非常大。

Prepare在高频查询中到底快不快?
很多人会陷入一个常见误区:他们写个循环测试Prepare,结果发现比普通的db.Query还慢。这并不奇怪——预处理本身就有额外的握手开销,而且如果使用短连接,句柄可能很快被回收。所以,直接给Prepare下“快”或“慢”的结论,需要结合具体场景。
那么它到底快不快?答案是:取决于你的查询频率和数据库驱动。如果只是偶尔查一次,不如直接用db.Query;但如果是成百上千次的重复查询,且驱动支持真正的服务端预编译,那么Prepare会减少SQL解析的开销,从而带来可观的速度提升。
怎么写才能让Prepare真正生效?
关键在于**复用返回的*sql.Stmt对象**,而不是在循环里反复Prepare再Close。正确的做法是:
- ✅ 正确做法:一次
db.Prepare,多次stmt.Exec或stmt.Query,最后再stmt.Close()。 - ❌ 常见错误:每次循环都调用
db.Prepare("SELECT ..."),然后立刻Close()——这样预处理的开销被不断重复,反而拖慢性能。 - ⚠️ 隐患:用完没有
Close()会导致连接泄漏。注意,sql.DB并不会自动回收*sql.Stmt,如果不手动关闭,它会持续占用数据库连接。
看一个正确示例:
stmt, err := db.Prepare("SELECT name FROM users WHERE id = ?")
if err != nil {
panic(err)
}
defer stmt.Close() // 必须,否则stmt持续占用连接
for _, id := range ids {
rows, err := stmt.Query(id)
// ...
}
MySQL驱动里prepare=true和multiStatements=false的区别
很多Go开发者在使用github.com/go-sql-driver/mysql时,默认走的是客户端模拟预处理,也就是把SQL拼接好后再发给服务端。这种情况下,Prepare只是语法糖,并不会减少服务端的解析开销。要启用真正的服务端预处理,需要显式加上参数:
parseTime=true&loc=UTC&multiStatements=false&clientFoundRows=true—— 注意multiStatements=false是必要条件,否则会禁用服务端prepare。- 如果连接串里加了
interpolateParams=true,则会强制走客户端拼接,彻底绕过服务端prepare。 - MySQL服务端版本低于5.7时,即使参数正确,prepare也可能退化为客户端行为。
如何验证是否生效?开启MySQL的general log,你会看到日志中间出现Prepare ...和Execute ...的记录,而不是连续的Query ...。这才是真正的服务端预编译。
并发场景下Stmt复用的坑
*sql.Stmt本身**不是goroutine安全的**,但sql.DB内部对Stmt做了连接池管理。所以多个goroutine同时调用同一个stmt.Query是安全的——只要不同时调用stmt.Close()。
- ⚠️ 危险操作:
go func(){ stmt.Close() }()和stmt.Query()同时执行,可能导致panic:“sql: statement is closed”。 - ✅ 安全做法:确保
stmt.Close()只在所有使用方结束后调用,或者用sync.Once来控制。 - ? 更稳妥的方案:如果并发高且SQL模板固定,优先考虑
db.QueryRow("SELECT ...", args...)。现代MySQL和PostgreSQL的查询缓存和计划缓存已经非常成熟,Prepare的优势被大幅削弱。
真正需要Prepare的场景其实只剩两种:一是超长生命周期的后台任务反复执行同一条SQL;二是SQL包含大量参数(比如IN列表动态拼接),且驱动支持executemany这类批量操作能力——此时Stmt可以配合Query实现批量参数传递。
