Prepare在高频查询中确实更快,但仅当同一SQL模板重复执行数百次以上且满足服务端预处理前提(如MySQL≥5.7、驱动正确配置)时才显著体现优势;单次或低频下因额外交互开销反而更慢。

Prepare在高频查询中到底快不快?
直接说结论:Prepare在单次或低频查询中几乎没收益,甚至更慢;但在**同一SQL模板重复执行数百次以上**时,才开始体现优势——前提是驱动支持真正的服务端预处理(如mysql驱动开启multiStatements=false且MySQL版本≥5.7),而sqlite3或pgx(非pool模式)等驱动行为差异极大。
常见误判来源:很多人用for i := 0; i 测<code>Prepare,结果发现比db.Query还慢——这很正常,因为预处理本身有额外握手开销,且短连接下句柄可能被回收。
怎么写才能让Prepare真正生效?
关键不是“调了Prepare”,而是**复用返回的*sql.Stmt对象**,并避免在循环里反复Prepare再Close:
- ✅ 正确:一次
db.Prepare,多次stmt.Exec/stmt.Query,最后stmt.Close() - ❌ 错误:每次循环都
db.Prepare("SELECT ..."),然后立刻Close() - ⚠️ 隐患:用完没
Close(),导致连接泄漏(sql.DB不会自动回收*sql.Stmt)
示例片段:
立即学习“go语言免费学习笔记(深入)”;
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批量参数)。



















