动态SQL是实现列名可变的唯一方式,需严格校验列名合法性并参数化传值防注入;MySQL用PREPARE/EXECUTE+正则校验,PostgreSQL用quote_ident()+format()更安全。

UPDATE语句里不能直接拼接列名,必须用动态SQL
SQL标准不允许把列名写成变量(比如 SET @col = 'value'),否则会报错 Unknown column '@col' in 'field list'。想让列名可变,只能构造字符串再执行——也就是动态SQL。这在存储过程里最常见,比如做通用字段更新、审计日志补全、或低代码平台的后端逻辑。
关键点是:列名和值都要参与拼接,但值必须用参数化方式防注入;而列名只能拼进SQL字符串,所以要严格校验合法性。
- 列名必须来自白名单或正则过滤(如只允许字母、数字、下划线,且不以数字开头)
- 不要用
CONCAT()直接拼用户输入,哪怕加了引号也危险 - MySQL 8.0+ 可用
VALIDATE_PASSWORD_STRENGTH()类思路类比,但列名校验得自己写REGEXP
MySQL中用PREPARE + EXECUTE拼接UPDATE语句
核心流程是:拼字符串 → 预编译 → 执行 → 释放。注意变量作用域——@sql 是会话级用户变量,必须在同一线程内完成三步。
SET @table = 'users';
SET @col = 'status';
SET @val = 'archived';
SET @id = 123;
<p>-- 白名单校验(生产环境必须加)
SELECT @col REGEXP '^[a-zA-Z<em>][a-zA-Z0-9</em>]*$' INTO @is_valid;
IF @is_valid = 0 THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Invalid column name';
END IF;</p><p>SET @sql = CONCAT('UPDATE ', @table, ' SET ', @col, ' = ? WHERE id = ?');
PREPARE stmt FROM @sql;
EXECUTE stmt USING @val, @id;
DEALLOCATE PREPARE stmt;这里 USING 后面的 @val 和 @id 是参数占位符的实际值,保证了值的安全;而 @col 和 @table 是拼进字符串的,所以前面必须校验。
PostgreSQL用EXECUTE配合quote_ident()防注入
PostgreSQL 更严谨:列名/表名必须用 quote_ident() 转义,否则哪怕拼字符串也会被SQL注入。它会自动加双引号并转义关键字(比如把 order 变成 "order")。
DO $$
DECLARE
tbl TEXT := 'products';
col TEXT := 'price';
val NUMERIC := 99.99;
id INT := 42;
BEGIN
-- quote_ident() 处理列名,quote_literal() 处理值(但值推荐用参数化)
EXECUTE format('UPDATE %I SET %I = $1 WHERE id = $2', tbl, col)
USING val, id;
END $$;%I 是 format() 的标识符占位符,等价于 quote_ident();$1/$2 是执行时传入的参数,不是字符串拼接。这是PG最安全的写法。
如果硬要用字符串拼值(不推荐),必须用 quote_literal(val),但遇到 NULL 会生成字符串 'NULL' 而非真正的 NULL,容易出错。
WHERE条件也动态时,别漏掉空值判断和SQL注入点
实际业务中,WHERE 字段也常可变(比如按 email 或 phone 更新)。这时 WHERE 子句也要拼,且必须处理:值是否为空、操作符是否合法(= / LIKE / IN)、IN 列表怎么展开。
- 空值不能直接拼
WHERE email = NULL,得写WHERE email IS NULL - 操作符必须白名单检查(
IN ('=', '!=', 'LIKE')),禁止用户传'; DROP TABLE users; -- - 如果支持
IN,数组参数得用unnest(ARRAY[...])或临时表,不能直接拼字符串列表
最易忽略的是:动态列更新后,可能触发触发器或生成错误的执行计划——特别是当列类型不一致时(比如把字符串塞进 INT 列),错误直到 EXECUTE 才暴露,调试成本高。

















