根本原因是把存储过程当成了“sql拼凑区”:参数无约束、错误不捕获、执行计划不稳、调试靠print、手动同步生产库,导致失去契约性而沦为技术债黑洞。

为什么存储过程越写越多,反而更难维护?
根本原因不是逻辑复杂,而是把存储过程当成了“SQL拼凑区”:参数没约束、错误不捕获、执行计划不稳、调试靠PRINT、改完还得手动同步到生产库。一旦业务规则变两次,就有人开始绕过存储过程直接写应用层SQL——这说明它已经失去契约性,不再是“可信赖的接口”,而成了技术债黑洞。
参数设计必须带类型和NULL约束
常见错误是定义@id varchar或@status int = NULL却不声明NOT NULL,导致隐式转换、索引失效、执行计划抖动。更隐蔽的问题是把可选参数全设成= NULL,然后在WHERE里写AND (@name IS NULL OR name = @name)——这种写法让优化器无法有效选择索引。
- 输入参数一律显式声明
NOT NULL,除非业务真允许空值;可选参数用= NULL,但WHERE条件改用CASE WHEN @name IS NOT NULL THEN name = @name ELSE 1=1 END保持SARGable - 避免
varchar不带长度,必须写成varchar(50);数字类型不用float存金额,用decimal(18,2) - 输出参数只返回单值(如计数、状态码),复杂结果集统一用
SELECT返回——客户端绑定更稳,也方便前端分页
别让EXEC(@sql)毁掉所有优化努力
EXEC(@sql)看似灵活,实则三重灾难:执行计划无法缓存、SQL注入面敞开、错误堆栈丢失行号。某次线上故障排查发现,一个EXEC拼接的动态查询在并发下平均响应时间飙升400%,只因每次调用都触发重新编译。
- 表名/列名必须动态时,先查
sys.tables或INFORMATION_SCHEMA做白名单校验,绝不直接拼接 - 条件组合多时,用
sp_executesql参数化:EXEC sp_executesql N'SELECT * FROM Orders WHERE Status = @status', N'@status TINYINT', @status = 1 - 能静态写的坚决不动态——比如状态过滤,用
IF @status = 1 ... ELSE IF @status = 2 ...比拼字符串安全得多
错误处理不能只靠RAISERROR
只在末尾写RAISERROR('失败', 16, 1)等于没处理:不知道错在哪一行、参数是什么值、事务是否已污染。更糟的是,有些存储过程里TRY...CATCH块里没调用ERROR_LINE()和ERROR_MESSAGE(),日志里只剩“执行失败”四个字。
- 每个
BEGIN TRY后必须配BEGIN CATCH,且CATCH里至少记录ERROR_NUMBER()、ERROR_LINE()、ERROR_MESSAGE() - 事务内出错前先查
XACT_STATE():值为-1必须ROLLBACK,0可忽略,1才能COMMIT - 禁用
RETURN -1这类裸返回,改用THROW 50001, '库存不足', 1,让调用方能按错误号分类处理
最难的不是写出能跑的存储过程,而是让它在三个月后被另一个人修改时,仍能一眼看懂边界、参数含义和失败路径。所有优化手段最终都指向一件事:让存储过程像函数一样有签名、有契约、有可观测性——而不是一段藏在数据库里的黑盒SQL。











