优先用sp_executesql拼接动态where,别用exec(@sql);所有可选参数声明为null,只拼条件骨架如'and user_id = @user_id',参数类型在n'@user_id int'中显式声明并严格传参。

直接结论:优先用 sp_executesql 拼接动态 WHERE,别用 EXEC(@sql);实在不想拼 SQL,就用 OR + IS NULL 或空字符串兜底,但要注意索引失效风险。
SQL Server 里怎么安全拼动态 WHERE?
核心是把条件结构和参数值彻底分开——结构拼进字符串,值全交给 sp_executesql 绑定。
- 所有可选参数(如
@user_id、@status、@start_date)声明为NULL允许输入 - 构建 SQL 字符串时只加条件骨架:
IF @user_id IS NOT NULL SET @sql += ' AND user_id = @user_id' - 参数声明字符串必须显式写出类型:
N'@user_id INT, @status VARCHAR(20)',不能漏掉N前缀 - 最后调用:
EXEC sp_executesql @sql, @params, @user_id = @user_id, @status = @status—— 参数名要和声明顺序一致 - 常见错误是写成
'WHERE name = ''' + @name + '''',这既不防注入,又让执行计划无法复用
SQL Server 不拼字符串也能做多条件?
可以,但代价是查询优化器可能放弃索引,尤其在字段有非空约束或存在大量 NULL 时。
- 典型写法:
WHERE (user_id = @user_id OR @user_id IS NULL) AND (status = @status OR @status = '') - 如果参数传
NULL表示“不限”,就用OR @param IS NULL;如果传空字符串表示“不限”,就用OR @param = '' - 注意:SQL Server 对这种写法的估计行数常不准,容易走全表扫描,大表慎用
- 不要写成
CASE赋值形式:WHERE user_id = CASE WHEN @user_id IS NULL THEN user_id ELSE @user_id END,这会强制每行计算,更慢
MySQL 存储过程怎么避免 CONCAT 拼值?
MySQL 不支持 sp_executesql 那种多参数绑定,但可以用 PREPARE ... EXECUTE USING 实现等效效果。
- 每个条件判断是否启用:
IF in_user_id IS NOT NULL THEN SET @sql = CONCAT(@sql, ' AND user_id = ?'); - 占位符
?的数量和顺序必须严格对应USING后的变量列表 - 错误示范:
CONCAT('WHERE status = ''', in_status, '''')—— 单引号、SQL 注入、隐式转换三重雷 - 变量列表要按
?出现顺序组装:SET @params = CONCAT(@params, IF(in_user_id IS NOT NULL, '@user_id,', ''), IF(in_status IS NOT NULL, '@status,', ''));,再EXECUTE stmt USING @user_id, @status;
PostgreSQL 动态字段名怎么防注入?
值能参数化,字段名不能。所以字段名必须白名单校验,不能直接拼。
- 错误做法:
EXECUTE 'SELECT * FROM orders WHERE ' || in_field || ' = $1' USING in_value;——in_field是用户输入,等于开放注入入口 - 正确做法:用
CASE或IF显式限定合法字段:IF in_filter_type = 'status' THEN filter_sql := 'status = $1'; ELSIF in_filter_type = 'amount' THEN filter_sql := 'amount > $1'; END IF; - 再用
format()拼主 SQL:EXECUTE format('SELECT * FROM orders WHERE %s', filter_sql) USING in_value; - 字段名白名单必须硬编码,不能从配置表或用户输入中读取
最麻烦的不是写法,而是不同数据库对“参数化”的边界定义不同:SQL Server 允许参数化整个值列表,MySQL 只认 ? 占位符顺序,PostgreSQL 连字段名都得手动筛。别图省事跳过白名单或硬拼字符串,线上被拖库往往就始于一个没校验的 in_field。











