该拆而不是硬改的信号是同一存储过程中混杂事务、日志、通知、监控逻辑;应按业务语义(如ByTimeout、ByUserRequest)而非SQL动词拆分,参数须严格一致,事务与错误处理统一收口至顶层。

怎么判断该拆而不是硬改
当 ALTER PROCEDURE 开始让你手抖、改一行要测整条链路、新人看三天还分不清哪段在查库存哪段在发通知,说明不是“代码长”,而是职责混杂。真正该拆的信号是:同一过程里同时出现 INSERT INTO event_log、UPDATE orders、EXEC send_sms_proc、SELECT ... FROM sys.dm_exec_requests —— 这已经不是存储过程,是事务+日志+通知+监控四合一的黑盒。
不拆的代价比你想象的大:每次加个新状态码,都要翻 200 行 IF/ELSE;临时表名从 #tmp1 写到 #tmp9 还没到事务块;错误码散落在三处 RAISERROR 里,应用层收到 50000 却不知道是库存不足还是网络超时。
按业务语义拆,别按 SQL 动词拆
错误做法是把一个订单取消过程机械切成 “查单”“校验”“更新”“记日志” 四个过程,结果每个都依赖重复的 @order_id 和 @user_id,参数传五遍,精度截断三次。正确切法是按谁在什么场景下需要什么结果来抽象:
-
usp_Order_CancelByTimeout:只响应定时任务调用,输入@cutoff_time DATETIME2(3),内部调用下层,返回@ret_code INT(如 2001=已取消,2002=已发货不可取消) -
usp_Order_CancelByUserRequest:响应前端按钮,额外校验用户权限、支付状态,调用同一套数据更新逻辑 -
usp_Order_UpdateStatus:纯数据层,只接收清洗后的@order_id CHAR(32)、@new_status CHAR(2),不做任何查询、不写日志、不抛业务错误
关键点:上层过程命名带业务动因(ByTimeout、ByUserRequest),下层命名带数据动作(UpdateStatus、ReserveInventory),一眼区分层级。
参数传递时最容易静默失败的三个地方
拆完调不通?大概率卡在参数上。SQL Server 不报错,但值早被吃掉:
- 字符串长度不一致:
@order_no VARCHAR(32)传进下层声明为@order_no VARCHAR(20),前 12 位直接丢,且无警告 - 日期精度错配:
GETDATE()返回DATETIME(毫秒级),下层用DATE接收,秒以下全归零,WHERE create_time > @dt可能漏掉最后几条 - 空格污染:
'A12345 '(尾部空格)传进来,下层直接拿去查主键,查不到也不报错,只返回空结果集
实操建议:所有跨层参数,上下声明必须完全一致;调试期在下层开头加 IF LEN(@param) != LEN(LTRIM(RTRIM(@param))) RAISERROR('param has trailing space', 16, 1);日期类统一用 DATETIME2(3),别碰 DATETIME。
事务和错误处理必须收口到最上层
常见错误是每个小过程都写 BEGIN TRAN,结果上层一回滚,@@TRANCOUNT 变成负数,后续任意语句都报 The transaction ended in the trigger. The batch has been aborted.。
正确做法只有一条:事务控制权绝不下放。
- 仅在顶层过程(如
usp_Order_CancelByUserRequest)里写BEGIN TRY / BEGIN TRAN - 所有下层过程(如
usp_Order_UpdateStatus)禁止出现BEGIN TRAN、COMMIT、ROLLBACK - 下层需局部原子性?用
SAVE TRAN @sp_name+ROLLBACK TO,但必须在上层文档里注明该 savepoint 名称及用途 - 错误处理统一用
TRY/CATCH,CATCH块里记录ERROR_NUMBER()、ERROR_MESSAGE()到日志表,再THROW向上传播
最易被忽略的是:拆完后,原来在巨石过程里靠变量隐式传递的状态(比如“是否已扣减积分”),现在得显式通过 OUTPUT 参数或临时表返回,否则上层无法决策下一步——这不是多此一举,是模块化必须付出的接口成本。










