sp_executesql 是 sql server 唯一安全的动态 sql 执行方式,通过参数化隔离值类变量防注入,而 exec(@sql) 拼接易致 sql 注入;标识符需 quotename 转义,where 条件应以 1=1 开头统一拼接,执行计划缓存依赖字符串完全一致。

sp_executesql 是 SQL Server 里唯一靠谱的动态执行方式
直接用 EXEC(@sql) 拼接变量值,等于把数据库大门敞开——@username = 'admin'' OR 1=1 --' 这种输入一进来就全崩。而 sp_executesql 能把值类参数(如 @status、@start_time)真正隔离在字符串外,靠参数绑定防注入。
常见错误现象:写 SET @sql = 'WHERE id = @id'; EXEC(@sql),结果报错 Msg 137, Level 15, State 2: 必须声明标量变量 "@id"——因为 EXEC 不认识外部变量。
-
sp_executesql的参数声明必须是NVARCHAR(MAX),否则截断导致语法错误 - 所有值类条件(
WHERE、ORDER BY中的值)必须进参数列表,不能拼进字符串 - 调用时参数顺序必须和声明顺序严格一致,否则传错值
表名、列名、排序字段这些“标识符”不能参数化,得 QUOTENAME
你没法把 @table_name 当成参数塞进 SELECT * FROM @table_name ——SQL Server 语法根本不允许。这类标识符必须拼进字符串,但直接拼就是高危操作。
正确做法是用 QUOTENAME(@table_name) 包裹,它会自动加方括号并转义非法字符。比如 @table_name = 'users; DROP TABLE users--' 经过 QUOTENAME 变成 [users; DROP TABLE users--],变成合法但无害的标识符。
- 别自己写
'[' + @table_name + ']',漏掉单引号或特殊字符就炸 - 白名单校验更稳妥:对有限的几个表名做
CASE WHEN @table_name IN ('orders','logs_202406') THEN ... - MySQL 里没
QUOTENAME,得用CONCAT('`', REPLACE(@col, '`', '``'), '`')手动转义
动态 WHERE 条件拼接容易 syntax error,别手写 AND
最常踩的坑是空参数导致 WHERE AND status = 1 或 WHERE 后面啥也没有——SQL 直接报错。别指望靠 IF @name IS NOT NULL 一个个追加 AND 字符串。
标准解法是初始化语句为 N'SELECT * FROM users WHERE 1=1',后续每个条件都以 AND 开头,不管前面有没有条件。SQL Server 优化器能忽略 1=1,不影响性能。
- 日期范围要单独处理:
IF @start_date IS NOT NULL SET @sql += N' AND created_at >= @start_date' - 多个 LIKE 条件别硬拼
%,统一在参数里加:SET @name_param = N'%' + @name + N'%' - 避免拼出
WHERE 1=1 AND结尾多一个AND,逻辑上没问题但看着难受
执行计划缓存失效比你想的更频繁
你以为只要参数一样,SQL Server 就会复用执行计划?错。只要拼出的 @sql 字符串有一个空格、字母大小写、字段顺序不同,就当全新语句编译。高频调用下 CPU 瞬间拉满。
QUOTENAME 输出稳定,但你自己拼的字段列表、ORDER BY 子句若没标准化,就容易击穿缓存。比如有时查 id, name,有时查 name, id,哪怕逻辑等价,计划也不共享。
- 固定字段顺序:始终按字母序排列,如
SELECT customer_id, order_date, total - 统一空格风格:所有
+拼接前后加空格,避免'WHERE'+@cond变成WHEREstatus=1 - 调试时用
SELECT @sql看最终字符串,确认两次调用是否完全一致
动态 SQL 的灵活性代价是控制粒度变细——对象名要 QUOTENAME,值要参数化,字符串要标准化,少一步就埋雷。










