预编译语句原理
预编译语句(Prepared Statement)可提升重复查询性能,本文解析 GORM 的预编译机制。
预编译基础
工作原理
Go
普通执行: SQL拼接 → 解析 → 编译 → 执行 → 返回
预编译执行: 编译一次 → 缓存 → 多次执行(仅传参数)
Go 底层支持
Go
// database/sql 提供预编译支持
stmt, err := db.Prepare("SELECT * FROM users WHERE id = ?")
defer stmt.Close()
// 执行时仅传参数
row := stmt.QueryRow(1)
GORM PrepareStmt 模式
启用方式
Go
// 方式1:全局启用
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
PrepareStmt: true,
})
// 方式2:会话级启用
tx := db.Session(&gorm.Session{PrepareStmt: true})
内部实现
Go
type StmtStore struct {
sync.RWMutex
queries map[string]Stmt
}
type Stmt struct {
*sql.Stmt
Score int // 访问计数,用于 LRU 淘汰
}
缓存流程:
Go
func (c *PreparedStmtDB) GetStmtConn(db *sql.DB, sql string) (*sql.Stmt, error) {
c.Lock()
defer c.Unlock()
if stmt, ok := c.queries[sql]; ok {
stmt.Score++ // 命中缓存,增加热度
return stmt.Stmt, nil
}
// 未命中,预编译新语句
s, err := db.Prepare(sql)
c.queries[sql] = Stmt{Stmt: s, Score: 1}
return s, err
}
性能分析
适用场景
| 场景 | 普通模式 | PrepareStmt | 说明 |
|---|---|---|---|
| 单次查询 | 快 | 慢 | 编译开销大于执行 |
| 同构批量查询 | 一般 | 快 | 编译一次,多次执行 |
| 高并发短查询 | 一般 | 快 | 避免重复编译,降低 CPU |
| 动态 SQL | 快 | 不适用 | 每次 SQL 不同,缓存失效 |
基准测试参考
Go
// 场景:循环执行相同查询 1000 次
// 普通模式: ~120ms
// PrepareStmt: ~45ms (提升约 62%)
缓存管理
连接池交互
Go
// 预编译语句与连接绑定
// GORM 内部维护连接级别的缓存
type ConnPool struct {
Pool ConnPool
StmtCache map[uint]*PreparedStmtDB // 每个连接独立缓存
}
清理策略
text
// 手动清理
db.Callback().Raw().Replace("gorm:raw", func(tx *gorm.DB) {
// 清除当前会话所有预编译缓存
tx.ConnPool.(*PreparedStmtDB).Reset()
})
注意事项
PrepareStmt会增加连接内存占用,低频查询场景不建议启用。
动态条件查询(如不同字段组合 WHERE)会导致缓存命中率低,反而浪费内存。
长连接下缓存累积需定期清理,避免 OOM。
事务内预编译语句与事务连接绑定,事务结束自动释放。
要点总结
- 预编译语句一次编译多次执行,适合高频同构查询
- GORM 通过
PrepareStmt: true启用内置缓存机制 - 内部使用
StmtStore按 SQL 字符串缓存,LRU 策略管理 - 动态 SQL 场景不适用,缓存命中率低
- 需关注内存占用,必要时手动清理缓存
存放路径:D:\git2\jwdev\articles\GORM\专家\源码分析与底层原理\预编译语句原理.md
📝 发现内容有误?点击此处直接编辑