动态拼接表名必须走prepare+execute,不能直接用变量;select * from @table_name会报error 1064,因表名是标识符而非值,只能先concat拼字符串(须反引号包裹)、校验存在性、再prepare/execute执行。

动态拼接表名必须走 PREPARE + EXECUTE,不能直接用变量
MySQL 存储过程里,SELECT * FROM @table_name 或 CREATE TABLE @tbl 会直接报错 ERROR 1064——语法错误。表名是标识符,不是值,不能被参数化。唯一合法路径是先拼字符串,再通过预处理执行。
常见错误现象:Unknown prepared statement(没 PREPARE 就 EXECUTE)、Incorrect arguments to EXECUTE(EXECUTE 后跟了字符串变量而非句柄)。
- 必须用
CONCAT()拼出完整 SQL 字符串,例如:SET @sql = CONCAT('SELECT * FROM `', table_name, '` WHERE id = ?'); - 表名、索引名、列名等含特殊字符(空格、短横线、中文)时,**必须用反引号包裹**,否则拼出来语法非法
- 拼完建议加一句
SELECT @sql;打印核对,避免漏空格、多单引号、少括号 - 字符集要统一:确保
@sql变量是utf8mb4编码,否则拼接中文字段值可能乱码或截断
WHERE 条件动态拼接:值用 ? + USING,标识符只能硬拼
条件里的值(如 status = 'active' 中的 'active')必须用 ? 占位符 + USING 绑定,这是防 SQL 注入的底线。但 status 这个列名、OR/AND 逻辑连接词、排序方向(ASC)、甚至 IS NULL 都不能用 ?,只能拼进 @sql 字符串。
-
USING后只能跟用户变量(@var),不能直接写存储过程参数(如p_status),得先赋值:SET @status := p_status; -
?个数和USING后变量个数必须严格一致,多一个少一个都报错 - 如果传入的值是
NULL,USING @val会让 MySQL 自动转成column IS NULL,而不是危险的column = NULL - 别信外部传来的列名参数,拼之前务必查
INFORMATION_SCHEMA.COLUMNS确认它真实存在且属于目标表
安全校验表名和列名是硬性前置步骤
不做白名单或元数据校验就拼表名,等于给攻击者开后门。MySQL 不会替你过滤,拼错一个字符就语法错误,拼错一个权限就执行失败,拼错一个校验就注入成功。
- 查表是否存在:
SELECT 1 FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = @tbl_name - 查列是否存在:
SELECT 1 FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = @tbl_name AND column_name = @col_name - 更稳妥的做法是白名单硬编码允许的列名,比如只接受
'id'、'name'、'status',其他一律拒绝 - 表名若来自日期分表(如
log_202610),需额外校验格式是否符合log_YYYYMM正则逻辑,防止拼出log_; DROP TABLE users;
句柄命名冲突会导致执行旧语句,分支多时尤其危险
很多人写 PREPARE stmt FROM @sql; 看似简洁,但在 IF/ELSE 多分支场景下,如果两个分支都用 stmt 这个句柄名,后一个 PREPARE 会覆盖前一个,而 EXECUTE stmt 可能执行的是上一分支准备的语句——结果完全不可控。
- 简单方案:每个分支用不同固定名,如
stmt_select、stmt_insert - 动态方案(MySQL 8.0.23+):用
EXECUTE IMMEDIATE直接执行字符串,跳过 PREPARE;但低版本只能靠命名隔离 -
DEALLOCATE PREPARE不是必须的,会话结束自动清理;但不释放不影响功能,重复PREPARE stmt也不报错,容易掩盖问题 - 最易忽略的一点:拼接
LIMIT偏移量或ORDER BY字段时,它们也是标识符,不能用?,必须校验后硬拼











