使用mssql2008新特性(存储过程参数类型使用"用户自定义表"来实现批量DML更新多表)解决项目里遇到的性能问题
使用MSSQL2008有一段时间了,因为平常参与项目开发一般都使用NHbernate、Entity Framework等这些ORM工具来做数据的存储,所以并没有花时间去学习mssql2008,还是用mssql2000那会儿积累的经验来使MSSQL2008。当然也是粗略的了解到MSSQL2008也有表分区啊、自定义数据类型等。代码重构前一次操作需要数分钟,甚至半小时的操作重构后只需数秒至十几秒,前端经常卡死Ajax调用后台方法,后台不返回执行结果,后台出现超时错误等。
找到代码重构需要解决的问题:
1、原有代码实现方式是Entity Framework和拼接SQL混合的方式、因为需要同时更新多个表,Entity Framework实现起来是异常的吃力啊,前同事喜欢拼接大段的TSQL脚本在代码里实现,我的直觉是自己熟悉数据库,第一个想的就是使用存储过程来优化。见下图写法:
看到的问题就是很混乱,没有事务控制,现在系统就是经常出现异常时有些数据提交了有些没提交,部分进了第一个表,第二个表里状态没更新,你懂的,如此如此那般那般,我得经常维护这个项目,一直都有事情做。呵呵...
2、解决前台页面传递过来的JSON里处理多条记录,需要使用循环并嵌套循环的问题,提高效率。
解决的办法:
昨天看到知识库里的博文:“SQL Server 高性能写入的一些总结”,最后部分的使用MSSQL2008表参数来达到批量高效插入记录,转念一箱如果使用表参数一次传入需要循环多次传递的参数不是提高了速度吗?测试下来果真OK,效率提升了不止一点点啊,数十倍的提升!
C#调存储过程:
#region 准备DataTable DataTable dt = new DataTable(); dt.Columns.Add(new DataColumn("RequestKeyID", typeof(Guid))); dt.Columns.Add(new DataColumn("EntityCode", typeof(string))); dt.Columns.Add(new DataColumn("Thirdparty", typeof(string))); dt.Columns.Add(new DataColumn("PayStatus", typeof(int))); dt.Columns.Add(new DataColumn("LoginUserName", typeof(string))); foreach (JavaScriptObject r in rows) { year = int.Parse(r["Year"].ToString()); month = int.Parse(r["Month"].ToString()); var row = dt.NewRow(); row["RequestKeyID"] = Guid.NewGuid(); row["EntityCode"] = r["Entity"].ToString(); row["Thirdparty"] = r["ThirdPart"].ToString(); row["PayStatus"] = status; row["LoginUserName"] = login; dt.Rows.Add(row); } #endregion using (WHSEntities db = new WHSEntities()) { db.AdoExecuteProc(WebConfigurationManager.ConnectionStrings["WHSEntities"].ConnectionString, "Proc_StreamBatchPay", new System.Data.SqlClient.SqlParameter[] { new System.Data.SqlClient.SqlParameter("@P_BatchRequestDataTable",SqlDbType.Structured){Value=dt}, new System.Data.SqlClient.SqlParameter("@PayYear",SqlDbType.Int){Value=year}, new System.Data.SqlClient.SqlParameter("@PayMonth",SqlDbType.Int){Value=month}, new System.Data.SqlClient.SqlParameter("@PayStatus",SqlDbType.Int){Value=status} }); }
存储过程:
ALTER proc [dbo].[Proc_StreamBatchPay] @P_BatchRequestDataTable DT_StreamBatchRequest READONLY , --临时表多个支付请求 @PayYear int , --支付的财务年 @PayMonth int, --支付的财务月 @PayStatus int --是支付1 还是拒绝0 /* ***XXXXX过程 Date: 2013-3-14 原因: 1、批量插入和更新,加快页面反映速度,原来代码是EF和SQL混搭没有事务处理和页面反馈慢 2、使用事务来保证数据一致性 3、操作日志痕迹的保留 */ AS DECLARE @EntityCode nvarchar(20) DECLARE @Thirdparty nvarchar(40) DECLARE @LoginUserName nvarchar(60) DECLARE @CountOfPaymentID int DECLARE @SerialID uniqueidentifier DECLARE @PaymentID uniqueidentifier BEGIN SET XACT_ABORT ON; SET NOCOUNT ON; BEGIN TRY --开启事务 BEGIN TRANSACTION; INSERT INTO dbo.FMS_WH_PaymentDetail SELECT NEWID(),@PayStatus,p.PaymentID,GETDATE(),p.Accrual,p.UpdateBy,GETDATE(),p.ThirdPart,p.Category FROM dbo.FMS_WH_Payment p LEFT JOIN @P_BatchRequestDataTable i ON p.Entity = i.EntityCode and p.ThirdPart = i.Thirdparty WHERE p.Year = @PayYear and p.Month = @PayMonth and p.Category = '3PLStream'; --支付 IF @PayStatus = 1 BEGIN UPDATE dbo.FMS_WH_Payment SET UpdateBy= p.LoginUserName,UpdateDate=GETDATE(),Actual=Accrual FROM @P_BatchRequestDataTable p WHERE Entity = p.EntityCode AND ThirdPart = p.Thirdparty AND Category = '3PLStream' AND Year = @PayYear AND Month = @PayMonth; SELECT p1.SerialID,p1.PaymentID,p1.OldYear,p1.OldMonth,p1.[Year],p1.[Month],p1.Entity,p1.ThirdPart into #PayMent1Temp FROM dbo.FMS_WH_Payment1 p1 WHERE p1.Year = @PayYear AND p1.Month = @PayMonth AND p1.Entity+'-'+p1.ThirdPart IN (SELECT E.EntityCode+'-'+E.Thirdparty FROM @P_BatchRequestDataTable E) AND p1.Category = '3PLStream'; --游标处理PayMent1里对应的记录 DECLARE CUR_PAYMENT1 CURSOR FOR SELECT SerialID,PaymentID FROM #PayMent1Temp OPEN CUR_PAYMENT1; FETCH NEXT FROM CUR_PAYMENT1 INTO @SerialID,@PaymentID; WHILE @@FETCH_STATUS = 0 BEGIN SELECT @CountOfPaymentID = COUNT(*) FROM FMS_WH_Payment1 WHERE PaymentID=@PaymentID; --没跨月,当月发生当月支付 IF @CountOfPaymentID = 1 BEGIN UPDATE FMS_WH_Payment1 SET Actual=Accrual WHERE SerialID = @SerialID; END --跨月,当月发生下月支付 IF @CountOfPaymentID = 2 BEGIN --更新发生月那笔payment1记录实际付款金额 UPDATE FMS_WH_Payment1 SET Actual=Accrual WHERE SerialID = @SerialID; --删除结转的那笔用于显示的payment1记录 DELETE FMS_WH_Payment1 WHERE PaymentID=@PaymentID AND OldMonth <> [Month]; END --跨月,当月发生下下月支付 原则上不允许流量跨2月 IF @CountOfPaymentID = 3 BEGIN --更新发生月那笔payment1记录实际付款金额 UPDATE FMS_WH_Payment1 SET Actual=Accrual WHERE PaymentID=@PaymentID AND OldMonth = [Month]; --删除结转的那2笔用于显示的payment1记录 DELETE FMS_WH_Payment1 WHERE PaymentID=@PaymentID AND OldMonth <> [Month]; END FETCH NEXT FROM CUR_PAYMENT1 INTO @SerialID,@PaymentID END CLOSE CUR_PAYMENT1; DEALLOCATE CUR_PAYMENT1; --操作成功,写日志 INSERT INTO fms_system_log(dtDate,sThread,sLevel,sLogger,sMessage,sException) SELECT GETDATE(),'DBLOG','INFO','dbProc','02|执行了'+CAST(@PayYear AS VARCHAR)+'\'+CAST(@PayMonth AS VARCHAR)+'批量流量费用支付操作。',''; END --拒绝 IF @PayStatus = 0 BEGIN UPDATE dbo.FMS_WH_Payment SET UpdateBy= p.LoginUserName,UpdateDate=GETDATE(),[status]=0 FROM @P_BatchRequestDataTable p WHERE Entity = p.EntityCode AND ThirdPart = p.Thirdparty AND Category = '3PLStream' AND Year = @PayYear AND Month = @PayMonth; --写日志 INSERT INTO fms_system_log(dtDate,sThread,sLevel,sLogger,sMessage,sException) SELECT GETDATE(),'DBLOG','INFO','dbProc','02|执行了'+CAST(@PayYear AS VARCHAR)+'\'+CAST(@PayMonth AS VARCHAR)+'批量流量费用拒绝支付的操作。',''; END --提交事务 COMMIT TRANSACTION; END TRY BEGIN CATCH IF (XACT_STATE()) = -1 BEGIN ROLLBACK TRANSACTION; --异常日志记录 INSERT INTO fms_system_log(dtDate,sThread,sLevel,sLogger, sMessage,sException) SELECT GETDATE(),'DBLOG','ERROR','dbProc', '异常代码:'+CAST(ERROR_NUMBER() AS VARCHAR)+'\异常消息:'+ERROR_MESSAGE(),CAST(ERROR_NUMBER() AS VARCHAR) END; IF (XACT_STATE()) = 1 BEGIN COMMIT TRANSACTION; END; END CATCH; END
这是特定业务写的存储过程没什么用处,也不需要看懂,就是看看表参数是怎么用的,怎么使用事务来优化控制数据的一致性。看存储过程里的注释即可。
别告诉哥EF怎么调整性能好,哥没那时间去学习,多年数据库编程经验哥自信在这个时刻ADO.NET对于我是最可靠的。
OK。搞定收工,主要是留下思路给自己以后回顾的,万一能帮到你也解决了类似问题,那我就更开心了!请不要说没有截图对比效率提升,无图无真相;是真不方便哦。没效果我就不会分享了。
"作者:" 数据酷软件工作室
"出处:" http://datacool.cnblogs.com
"专注于CMS(综合赋码系统),MES,WCS(智能仓储设备控制系统),WMS,商超,桑拿、餐饮、客房、足浴等行业收银系统的开发,15年+从业经验。因为专业,所以出色。"
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++