SQL Server游标全表更新时EndDate值错误的问题问询
问题分析与解决方案
这个问题我之前也碰到过,核心原因是循环中的变量未重置导致旧值污染,结合你的代码详细拆解下:
为什么全表执行会出错?
看你的代码,@pId、@pInvItemId这些变量是在循环外声明的,每次循环时如果执行SELECT TOP 1 @pId = Id...没有找到匹配的记录(比如第一次处理某个新的InvItemId),这些变量会保留上一次循环的旧值。
举个实际场景的例子:
- 先处理
InvItemId=A,找到了对应的历史记录,@pId被赋值为100,然后执行UPDATE把这条记录的EndDate改成A的@CreatedOn。 - 接着处理
InvItemId=B,此时InvPrice_History里还没有B的记录,SELECT TOP 1不会给@pId赋值,所以@pId还是100(A的记录ID)。 - 这时候执行
UPDATE InvPrice_History SET EndDate = CONVERT(VARCHAR, @CreatedOn) WHERE Id = @pId;,就会错误地把A的那条记录的EndDate改成B的@CreatedOn。
全表执行时,这种跨Item的变量污染会反复发生,最终大量记录的EndDate被最后处理的几条记录的@CreatedOn覆盖;而指定单个ItemId时,所有循环都处理同一个Item,不会出现其他Item的旧值残留,所以结果正确。
另外还有个小隐患:CONVERT(VARCHAR, @CreatedOn)没有指定VARCHAR的长度,SQL Server在不同上下文的默认长度可能不一致,可能导致日期字符串截断,建议显式指定长度(比如VARCHAR(23))来匹配DATETIME的完整格式。
修复方案
1. 每次循环重置变量
在每次循环开始时,把@pId、@pInvItemId等相关变量重置为NULL,避免旧值干扰:
FETCH NEXT FROM stockCursor INTO @Id, @InvItemId, @LocationId, @IsDeleted, @Quantity, @CreatedOn, @StandardCost; WHILE @@FETCH_STATUS = 0 BEGIN -- 重置历史记录变量为NULL SET @pId = NULL; SET @pInvItemId = NULL; SET @pStartDate = NULL; SET @pEndDate = NULL; SET @pCost = NULL; SELECT TOP 1 @pId = Id, @pInvItemId = InvItemId, @pStartDate = StartDate, @pEndDate = EndDate, @pCost = Cost FROM InvPrice_History WHERE InvItemId = @InvItemId ORDER BY StartDate DESC -- 后续逻辑保持不变... END
2. 显式指定CONVERT长度
把CONVERT(VARCHAR, @CreatedOn)改成CONVERT(VARCHAR(23), @CreatedOn),确保日期格式完整:
UPDATE InvPrice_History SET EndDate = CONVERT(VARCHAR(23), @CreatedOn) WHERE Id = @pId;
3. 增加@pId非空判断(可选但更严谨)
在执行UPDATE前先检查@pId是否不为NULL,避免无意义的执行:
IF @pId IS NOT NULL AND @pCost != @sStandardCost BEGIN UPDATE InvPrice_History SET EndDate = CONVERT(VARCHAR(23), @CreatedOn) WHERE Id = @pId; END
修改后的完整代码示例
DECLARE @up_InvItemId INT = 14330 TRUNCATE TABLE InvPrice_History --DELETE FROM InvPrice_History where InvItemId=@up_InvItemId DECLARE stockCursor CURSOR LOCAL FOR SELECT Id, InvItemId, LocationId, IsDeleted, Quantity, CreatedOn, StandardCost FROM InvStockMoves --WHERE InvItemId = @up_InvItemId ; DECLARE @Id INT, @InvItemId INT, @LocationId INT, @IsDeleted BIT, @Quantity FLOAT, @CreatedOn DATETIME, @StandardCost FLOAT; DECLARE @pId INT, @pInvItemId INT, @pLocationId INT, @pStartDate DATETIME, @pEndDate DATETIME, @pCost FLOAT; DECLARE @sId INT, @sInvItemId INT, @sLocationId INT, @sIsDeleted BIT, @sQuantity FLOAT, @sCreatedOn DATETIME, @sStandardCost FLOAT; OPEN stockCursor; FETCH NEXT FROM stockCursor INTO @Id, @InvItemId, @LocationId, @IsDeleted, @Quantity, @CreatedOn, @StandardCost; WHILE @@FETCH_STATUS = 0 BEGIN -- 重置历史记录变量,避免旧值污染 SET @pId = NULL; SET @pInvItemId = NULL; SET @pStartDate = NULL; SET @pEndDate = NULL; SET @pCost = NULL; SELECT TOP 1 @pId = Id, @pInvItemId = InvItemId, @pStartDate = StartDate, @pEndDate = EndDate, @pCost = Cost FROM InvPrice_History WHERE InvItemId = @InvItemId ORDER BY StartDate DESC SET @sStandardCost = dbo.getByTimeCostInvItem(@CreatedOn, @InvItemId); IF @sStandardCost IS NOT NULL AND @sStandardCost > 0 BEGIN IF @pId IS NOT NULL AND @pCost != @sStandardCost BEGIN IF @InvItemId = @up_InvItemId BEGIN PRINT(@CreatedOn); END UPDATE InvPrice_History SET EndDate = CONVERT(VARCHAR(23), @CreatedOn) WHERE Id = @pId; END IF @pId IS NULL OR (@pId IS NOT NULL AND @pCost != @sStandardCost) BEGIN INSERT INTO InvPrice_History (InvItemId, StartDate, EndDate, Cost) VALUES (@InvItemId, @CreatedOn, NULL, @sStandardCost); END END FETCH NEXT FROM stockCursor INTO @Id, @InvItemId, @LocationId, @IsDeleted, @Quantity, @CreatedOn, @StandardCost; END CLOSE stockCursor DEALLOCATE stockCursor
内容的提问来源于stack exchange,提问作者Maaz Anzar
相关产品推荐
相关产品推荐

