最稳的动态sql写法是用if判断拼接where条件并加option(recompile),配合显式类型声明和session_context安全传参;itvf适用于固定过滤逻辑,但必须为纯select。

直接在存储过程中拼接 WHERE 条件是可行的,但必须避开 sp_executesql 的参数绑定陷阱和参数嗅探失效问题,否则查得慢、查不准、甚至漏数据。
存储过程里用 IF 判断拼 WHERE 是最稳的写法
动态条件本质是“有就加,没有就跳过”,硬拼字符串反而比试图用统一参数更可靠。关键不是避免动态 SQL,而是控制好执行计划和类型安全。
- 每个条件独立判断,用
IF @param IS NOT NULL控制是否追加,不依赖OR 1=1这类兜底逻辑(容易让索引失效) - 拼接时注意空格和换行:开头带空格的
AND,结尾不加多余空格,避免语法错误 - 所有参数都显式声明类型,传给
sp_executesql时类型必须完全匹配,比如@tenant_id INT就不能传'123'字符串 - 示例片段:
DECLARE @sql NVARCHAR(MAX) = N'SELECT * FROM orders WHERE 1=1';<br>IF @tenant_id IS NOT NULL SET @sql += N' AND tenant_id = @tenant_id';<br>IF @status IS NOT NULL SET @sql += N' AND status = @status';<br>EXEC sp_executesql @sql, N'@tenant_id INT, @status VARCHAR(20)', @tenant_id, @status;
不加 OPTION(RECOMPILE) 很可能查错数据
SQL Server 默认会缓存第一次执行的执行计划,如果首次用 @tenant_id = 1 编译,后续查 @tenant_id = 999 可能仍走老计划——尤其是当字段选择性差或统计信息滞后时。
- 对动态条件多、参数变化大的存储过程,强制加
OPTION (RECOMPILE)是成本可控的保险做法 - 加在最终
SELECT后,不是拼 SQL 字符串里:EXEC sp_executesql @sql, @params, @tenant_id, @status OPTION (RECOMPILE); - 不用怕“每次重编译开销大”:现代 SQL Server 对简单查询的重编译耗时通常低于 1ms,远小于因计划偏差导致的秒级延迟
- 若担心高并发下 CPU 压力,可按条件组合数做分级——比如只对
@tenant_id + @status组合加RECOMPILE,单条件走缓存
视图不能传参,但 ITVF 可以且性能不输视图
如果过滤逻辑固定(如“按租户+状态查订单”),别硬塞进存储过程拼 SQL,改用内联表值函数(ITVF)更干净、更易复用、优化器也更友好。
- 定义:
CREATE FUNCTION dbo.fn_orders(@tenant_id INT, @status VARCHAR(20)) RETURNS TABLE AS RETURN (SELECT * FROM orders WHERE tenant_id = @tenant_id AND status = @status); - 调用:
SELECT * FROM dbo.fn_orders(123, 'shipped');,支持 JOIN、WHERE 下推,执行计划和直接查表几乎一致 - 严禁用多语句 TVF(MTVF):它返回临时结果集,阻断优化器,大数据量时性能断崖下跌
- ITVF 函数体只能是单个
SELECT,不能有变量、循环、BEGIN...END,否则就退化成 MTVF
SESSION_CONTEXT 必须由应用层设置,且每次连接都要重置
存储过程本身无法获取当前业务用户身份,ORIGINAL_LOGIN() 或 SUSER_SNAME() 返回的是数据库账号,不是你 App 里的 user_id。真正可用的只有 SESSION_CONTEXT,但它不会自动存在。
- 应用层(如 C#、Java、PHP)在获取数据库连接后,必须立即执行:
EXEC sp_set_session_context @key=N'user_id', @value=N'abc123'; - 存储过程里读取:
WHERE user_id = CAST(SESSION_CONTEXT(N'user_id') AS VARCHAR(50)),注意必须CAST,因为返回类型是SQL_VARIANT - 未设置时
SESSION_CONTEXT(N'user_id')返回NULL,所以 WHERE 条件要处理空值,例如:user_id = ISNULL(CAST(SESSION_CONTEXT(N'user_id') AS VARCHAR(50)), 'nobody') - 连接池场景下尤其危险:上一个请求设的
SESSION_CONTEXT可能被下一个请求复用,务必在每次连接初始化时重置或清空
最容易被忽略的是 SESSION_CONTEXT 的生命周期管理——它只活在当前连接里,但连接池会让这个“当前连接”变得不可预测;拼 SQL 时漏掉 OPTION (RECOMPILE) 看似省事,实则埋下性能雷;而把 ITVF 当成“高级视图”用,却忘了它必须是纯 SELECT,加个变量赋值就全毁。











