MySQL2中execute与query的区别及最佳实践
2025-03-05 09:09:50
mysql2 中 execute 和 query 的区别与使用场景
遇到 NodeJS mysql2 库的 connection.execute()
和 connection.query()
方法时, 你可能会疑惑它们的区别。简单来说, 就是预处理语句和参数的处理方式不同。
问题的根源:预处理语句
要理解这两者的差别, 先要弄明白什么是“预处理语句”。
MySQL 数据库支持预处理语句,这是一种优化数据库查询性能的方式。预处理语句,就是把 SQL 语句的模板先发送给数据库进行编译, 之后只需要发送参数, 就能执行这条 SQL, 不需要每次都重新编译 SQL 语句。
好处很明显:
- 性能提升 : 少了重复编译 SQL 语句的开销,特别是在循环执行同一条 SQL 语句时, 效果显著。
- 安全 : 能有效防止 SQL 注入。因为参数是单独发送的,数据库会正确处理参数,不会把参数当成 SQL 语句的一部分来执行。
execute
和 query
的不同之处
mysql2 提供了两种执行 SQL 的方式:
connection.query()
: 更像传统的方式,一次性发送完整的 SQL 语句和参数。connection.execute()
: 使用预处理语句。先发送 SQL 模板给 MySQL, MySQL 编译好等着。然后, 再把参数传过去执行。
用提供的例子来看看,这是三个不同的函数:
async function B1(connection) {
const statement = await connection.prepare("SELECT 1 + ? + ?");
const result1 = await statement.execute([1, 2]);
const result2 = await statement.execute([3, 4]);
await statement.close();
return [result1, result2];
}
async function B2(connection) {
const result1 = await connection.execute("SELECT 1 + ? + ?", [1, 2]);
const result2 = await connection.execute("SELECT 1 + ? + ?", [3, 4]);
return [result1, result2];
}
async function B3(connection) {
const result1 = await connection.query("SELECT 1 + ? + ?", [1, 2]);
const result2 = await connection.query("SELECT 1 + ? + ?", [3, 4]);
return [result1, result2];
}
B1:手动预处理
B1
函数, 先用 connection.prepare()
手动创建一个预处理语句。之后, 多次调用 statement.execute()
并传入不同的参数,来复用这个预处理语句。 这就是标准的预处理语句用法,性能好,还安全。
B2: connection.execute()
的隐式复用?
问题来了,B2
中, 两次调用 connection.execute()
, 每次都用同一个 SQL 模板 "SELECT 1 + ? + ?"。 mysql2 会不会自动帮我们复用预处理语句,省去重复编译的开销?
答案是: 会!
mysql2 内部会维护一个预处理语句缓存。当 connection.execute()
被调用时, 它会:
- 检查缓存:看看有没有和当前 SQL 模板匹配的已经准备好的语句。
- 如果有, 直接用缓存的语句,发送参数,执行。
- 如果没有, 就发送 SQL 模板去编译, 存到缓存里,然后再发送参数执行。
所以, B2
其实比 B3
更高效. B2
在第二次执行时可以利用缓存, 只需要发送参数. 而 B3
每次都要发送完整的 SQL 和参数, 没有预处理的优化。
B3: connection.query()
B3
用了 connection.query()
, 没有预处理, 每次都发送完整的 SQL 语句和参数给 MySQL,开销比较大。
性能对比与选择
所以, 性能对比大致是: B1 ≈ B2 > B3。
- 如果需要手动控制预处理语句的生命周期 (比如需要用到
statement.close()
),用 B1 的方式。 - 如果只需要简单执行,用
connection.execute()
(像B2),mysql2 会自动处理缓存。 - 尽量避免
connection.query()
搭配参数化查询的方式 (像 B3), 因为它享受不到预处理语句的好处。如果只是单次执行, 不需要参数的 SQL,query()
可以用。
进阶技巧与注意事项
缓存大小限制
mysql2 的预处理语句缓存有大小限制, 达到限制后, 会使用 LRU (Least Recently Used) 算法来淘汰旧的语句。可以用connection.config.namedPlaceholders
与 connection.config.namedPlaceholders
查看当前的缓存大小, 可以用如下方式在链接的时候进行设置:
const pool = mysql.createPool({
host: 'localhost',
user: 'root',
database: 'test',
waitForConnections: true,
connectionLimit: 10,
queueLimit: 0,
namedPlaceholders: true, //启用命名占位符,与缓存大小限制息息相关
preparedStatementCacheSize: 250, //自定义缓存的大小.默认是25
});
preparedStatementCacheSize
参数控制最大数量, 超过就淘汰. 如果确定预处理语句类型不多,且调用频繁, 增大这个值会好一点。
命名参数
除了使用 ?
作为占位符, mysql2 还支持命名参数:
const result = await connection.execute(
'SELECT * FROM users WHERE id = :id AND status = :status',
{ id: 123, status: 'active' }
);
这样, SQL 更容易阅读, 特别是参数比较多的时候。
什么时候用 query()
?
虽说 execute()
通常更好, 但有些情况用 query()
也行:
- 一次性查询 : 如果只执行一次, 没必要预处理。
- 不带参数的查询 : 比如
SELECT VERSION()
这种,不需要预处理。 - 执行非
SELECT
/INSERT
/UPDATE
/DELETE
语句 : 有些命令, 比如SET
,SHOW
, 某些情况下不一定能用预处理语句。
安全建议
不管用 execute()
还是 query()
, 都要用参数化查询, 避免直接拼接 SQL 字符串, 这是防止 SQL 注入的铁律!
// 千万不要这样做!
const userId = req.query.userId;
const sql = "SELECT * FROM users WHERE id = " + userId;
const result = await connection.query(sql); // 大错特错!
总结下, 在使用 mysql2 时,推荐尽量用 connection.execute()
, 让库自动管理预处理语句, 既安全又高效。记住, 用参数化查询, 不要直接拼接字符串!