预处理语句是什么
预处理把 SQL 的「结构」与「数据」分开:先 PREPARE 一条带占位符的语句让服务端解析成执行计划,再用 EXECUTE 反复传入参数执行。对应用来说,真正重要的是第二条:用占位符传参是防 SQL 注入的根本手段,因为参数永远被当成数据解析,不会被拼进语法结构里。服务端 PREPARE 是 SQL 层的工作方式,应用层的参数化查询由驱动实现,二者不要混为一谈。
基本信息
| SQL 层语法 | PREPARE 名 FROM '语句';EXECUTE 名 USING 变量;DEALLOCATE PREPARE 名 |
|---|---|
| 变量形式 | EXECUTE USING 只接受用户变量(@var)或字面量,不接受表达式 |
| 作用域 | 预处理语句是会话级的,断开连接即失效 |
| 数量上限 | max_prepared_stmt_count(默认 16382),泄漏句柄会打满 |
| 语句限制 | 只有文档列出的语句类型可预处理,动态表名与列名不能参数化 |
典型使用场景
- 防注入:应用所有带用户输入的查询统一走占位符,杜绝字符串拼接
- 一次解析多次执行:批量写入或循环查询时复用同一条语句,省去重复解析
- SQL 层临时脚本:在 mysql 客户端里用 PREPARE 拼装需要动态条件的语句做临时核对
- 安全审计:发现代码里仍存在拼接 SQL 时,改用参数化并配合代码扫描固化规则
- 连接池行为验证:确认驱动是否真的使用了服务端预处理,还是仅在客户端做转义
SQL 层与错误用法对照
SQL 层与错误用法对照
-- 正确:占位符只用于「值」
PREPARE stmt FROM 'SELECT id, sku FROM orders WHERE user_id = ? LIMIT ?';
SET @uid = 42, @n = 10;
EXECUTE stmt USING @uid, @n;
DEALLOCATE PREPARE stmt; -- 用完立刻释放,避免句柄泄漏
-- 错误:想让占位符充当表名,服务端不接受这种参数
-- PREPARE bad FROM 'SELECT * FROM ?';
-- 动态表名只能在白名单校验后拼接
SET @tbl = 'orders_2026';应用层参数化
应用层参数化
# 参数与语句分开传,驱动负责转义与协议编码
cur.execute('SELECT id, sku FROM orders WHERE user_id = %s AND status = %s', (uid, status))
# 反例:f-string 拼接等于把注入入口留在代码里
cur.execute(f"SELECT * FROM orders WHERE sku = '{sku}'")拼接与参数化对照
| 维度 | 手工拼接字符串 | 占位符参数化 |
|---|---|---|
| 注入风险 | 随输入内容变化,始终存在 | 参数只作数据解析,注入不可达 |
| 计划复用 | 每个不同值都是新语句 | 同一语句可复用计划,高频查询更省 |
| 可读性 | 引号与转义层层叠加 | 语句与参数分离,容易审阅 |
| 适用边界 | 仅动态标识符场景(需白名单) | 所有可传值的位置 |
服务端预处理在连接内有效。使用连接池时,连接可能被复用给别的请求,长事务与异常路径都要确保语句被 DEALLOCATE,否则句柄会累积到 max_prepared_stmt_count 上限报错。
参数化不覆盖标识符:表名、列名、ORDER BY 字段这类位置无法用占位符。需要动态拼接时必须做白名单校验(只允许预定义集合里的名字),不要仅依赖转义函数。