mysql2 的 execute 吃动态 SQL 会炸:用 query 才是正解
PATCH 接口写完测试,第一次请求全字段时一切正常,第二次只传两个字段——500。日志里是一行陌生的 mysql2 报错,Google 搜出来的结果全是 ORM 的坑,半天才意识到问题在 pool.execute 上。
现象
PATCH /api/settings/users在部分字段更新时返回 500- 错误栈显示:
Error: Incorrect arguments to mysqld_stmt_execute - 同一接口,传全字段能成功,只传部分字段就崩
- 动态
IN (?, ?, ?)的过滤接口,列表长度变化时同样挂掉
根因
pool.execute() 走的是 MySQL 服务端预处理语句(server-side prepared statements),使用二进制协议。MySQL 服务端会把 SQL 文本缓存下来,第一次 prepare 之后后续调用复用。
问题在于:动态拼接的 SQL(比如 SET a=?, b=? 和 SET a=? 是两条完全不同的 SQL 文本)每次都要重新 prepare,而 mysql2 在某些场景下会出现 placeholder 数量与绑定参数数量对不上的错误,或者因为 SQL 文本频繁变化导致 prepare cache 抖动。
动态表名、动态列名插值进 SQL 的情况更危险——这类标识符无法参数化,一旦插值进去就变成了不同的 SQL 文本,绑定行为难以预测。
pool.query() 走的是文本协议 + 客户端插值,直接把参数替换进去发给 MySQL,天然容忍 SQL 文本按需变化。
pool.execute 的服务端预处理机制假设 SQL 文本是固定的。只要 SQL 里的占位符数量随业务逻辑变化(部分字段更新、可变长 IN 列表、动态表名),就会触发 Incorrect arguments to mysqld_stmt_execute 或静默绑定错位。
适用于 mysql2/promise v3.x。
正解
把动态 SQL 的 pool.execute 换成 pool.query,其他不变,参数化占位符 ? 照用:
// 修改前(在字段数量可变时会崩)
if (updates.length > 0) {
params.push(body.id)
await pool.execute(
`UPDATE users SET ${updates.join(', ')} WHERE id=?`,
params
)
}
// 修改后
if (updates.length > 0) {
params.push(body.id)
await pool.query(
`UPDATE users SET ${updates.join(', ')} WHERE id=?`,
params
)
}可变长 IN 列表同理:
// 修改前
await pool.execute(`SELECT * FROM orders WHERE status IN (${placeholders})`, statuses)
// 修改后
await pool.query(`SELECT * FROM orders WHERE status IN (${placeholders})`, statuses)经验规则:
- 静态 SQL(列名、表名、占位符数量完全固定)→ 用
execute,性能更好,服务端 prepare 缓存有效 - 动态 SQL(字段集合可变、IN 列表长度可变、有动态表名/列名插值)→ 用
query
两者都应保留 ? 占位符传参,绝不直接把用户输入插值进 SQL 字符串。
一句话外卖
mysql2 的
execute是为"SQL 模板固定、参数不同"设计的;只要 SQL 文本本身会变,就换query。