SQL语句优化:While循环的正确使用与性能优化
在SQL开发中,循环处理是常见的需求,但While循环如果使用不当,会严重影响性能。今天我们来深入探讨如何正确使用While循环,以及如何优化循环操作。
-- 基础语法DECLARE @counter INT = 1DECLARE @maxCount INT = 10WHILE @counter <= @maxCountBEGIN -- 执行操作 PRINT '当前计数:' + CAST(@counter AS VARCHAR) -- 更新计数器(重要:避免死循环) SET @counter = @counter + 1END
-- ❌ 错误示范:逐行插入(性能极差)CREATE TABLE #BadExample (ID INT, Name VARCHAR(50))DECLARE @i INT = 1WHILE @i <= 100000BEGIN INSERT INTO #BadExample VALUES (@i, 'Name_' + CAST(@i AS VARCHAR)) SET @i = @i + 1END-- 耗时:约30-60秒-- ✅ 优化方案1:批量插入(每次1000条)CREATE TABLE #GoodExample (ID INT, Name VARCHAR(50))DECLARE @i INT = 1DECLARE @BatchSize INT = 1000DECLARE @MaxCount INT = 100000WHILE @i <= @MaxCountBEGIN INSERT INTO #GoodExample (ID, Name) SELECT TOP (@BatchSize) @i + ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) - 1 AS ID, 'Name_' + CAST(@i + ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) - 1 AS VARCHAR) AS Name FROM sys.objects a, sys.objects b -- 生成临时数据源 WHERE @i <= @MaxCount SET @i = @i + @BatchSizeEND-- 耗时:约2-5秒,性能提升10倍以上-- ✅ 优化方案2:一次性插入(最佳方案)CREATE TABLE #BestExample (ID INT, Name VARCHAR(50));WITH Numbers AS ( SELECT TOP 100000 ROW_NUMBER() OVER (ORDER BY a.object_id) AS n FROM sys.objects a CROSS JOIN sys.objects b)INSERT INTO #BestExample (ID, Name)SELECT n, 'Name_' + CAST(n AS VARCHAR)FROM Numbers-- 耗时:约0.5秒,性能提升100倍以上
-- ❌ 错误示范:逐行更新(性能极差)DECLARE @id INTDECLARE cur CURSOR FOR SELECT ID FROM LargeTableOPEN curFETCH NEXT FROM cur INTO @idWHILE @@FETCH_STATUS = 0BEGIN UPDATE LargeTable SET UpdateTime = GETDATE() WHERE ID = @id FETCH NEXT FROM cur INTO @idENDCLOSE curDEALLOCATE cur-- 10万条数据耗时:约2-3分钟-- ✅ 优化方案:分批更新(推荐)DECLARE @BatchSize INT = 5000DECLARE @MinID INTDECLARE @MaxID INTSELECT @MinID = MIN(ID), @MaxID = MAX(ID) FROM LargeTableWHILE @MinID <= @MaxIDBEGIN UPDATE LargeTable SET UpdateTime = GETDATE() WHERE ID >= @MinID AND ID < @MinID + @BatchSize SET @MinID = @MinID + @BatchSize -- 可选:添加延迟减少锁竞争 WAITFOR DELAY '00:00:00.100'END-- 10万条数据耗时:约2-5秒-- ✅ 优化方案2:直接批量更新(如果可以一次完成)UPDATE LargeTable SET UpdateTime = GETDATE()-- 10万条数据耗时:约0.5秒
-- ❌ 错误示范:一次性删除(可能导致事务日志爆满)DELETE FROM LargeTable WHERE CreateDate < '2024-01-01'-- 100万条数据可能导致日志暴涨或超时-- ✅ 优化方案:分批删除(推荐)DECLARE @BatchSize INT = 10000DECLARE @RowCount INT = 1WHILE @RowCount > 0BEGIN DELETE TOP (@BatchSize) FROM LargeTable WHERE CreateDate < '2024-01-01' SET @RowCount = @@ROWCOUNT -- 输出进度(可选) PRINT '已删除 ' + CAST(@@ROWCOUNT AS VARCHAR) + ' 条记录' -- 检查点:减少日志压力(非生产环境慎用) IF @RowCount > 0 AND @RowCount % 50000 = 0 CHECKPOINTEND
-- 创建示例表CREATE TABLE Orders ( OrderID INT PRIMARY KEY, OrderDate DATETIME, Status INT DEFAULT 0, ProcessTime DATETIME NULL)-- 插入测试数据INSERT INTO Orders (OrderID, OrderDate)SELECT TOP 100000 ROW_NUMBER() OVER (ORDER BY a.object_id), GETDATE()FROM sys.objects a, sys.objects b-- ✅ 优化方案:带状态标记的分批处理CREATE PROCEDURE usp_ProcessOrdersASBEGIN DECLARE @BatchSize INT = 1000 DECLARE @ProcessedCount INT = 0 -- 先标记待处理数据 UPDATE Orders SET Status = 1 WHERE Status = 0 AND OrderDate < DATEADD(day, -7, GETDATE()) WHILE 1 = 1 BEGIN -- 批量处理 UPDATE TOP (@BatchSize) Orders SET Status = 2, ProcessTime = GETDATE() OUTPUT inserted.OrderID, inserted.OrderDate INTO #ProcessedLog (OrderID, OrderDate) WHERE Status = 1 SET @ProcessedCount = @ProcessedCount + @@ROWCOUNT -- 检查是否处理完 IF @@ROWCOUNT < @BatchSize BREAK -- 可选:添加延迟减少系统压力 WAITFOR DELAY '00:00:00.500' END -- 记录处理结果 INSERT INTO ProcessLog (ProcessDate, RecordCount) VALUES (GETDATE(), @ProcessedCount) SELECT @ProcessedCount AS [处理记录数]END
-- ❌ 错误:每次循环都查询系统表DECLARE @i INT = 1WHILE @i <= 1000BEGIN IF EXISTS (SELECT 1 FROM sys.objects WHERE name = 'Table_' + CAST(@i AS VARCHAR)) BEGIN PRINT 'Table_' + CAST(@i AS VARCHAR) + ' 已存在' END SET @i = @i + 1END-- ✅ 优化:一次查询,临时存储DECLARE @i INT = 1DECLARE @Tables TABLE (TableName VARCHAR(100))INSERT INTO @Tables SELECT name FROM sys.objects WHERE name LIKE 'Table_%'WHILE @i <= 1000BEGIN IF EXISTS (SELECT 1 FROM @Tables WHERE TableName = 'Table_' + CAST(@i AS VARCHAR)) BEGIN PRINT 'Table_' + CAST(@i AS VARCHAR) + ' 已存在' END SET @i = @i + 1END
-- ❌ 使用游标(性能差)DECLARE cur CURSOR FOR SELECT ID, Name FROM LargeTableOPEN curDECLARE @id INT, @name VARCHAR(100)FETCH NEXT FROM cur INTO @id, @nameWHILE @@FETCH_STATUS = 0BEGIN -- 处理逻辑 FETCH NEXT FROM cur INTO @id, @nameENDCLOSE curDEALLOCATE cur-- ✅ 使用WHILE + 临时表(性能好)CREATE TABLE #Data (ID INT PRIMARY KEY, Name VARCHAR(100))INSERT INTO #Data SELECT ID, Name FROM LargeTableDECLARE @id INT, @name VARCHAR(100)WHILE EXISTS (SELECT 1 FROM #Data)BEGIN SELECT TOP 1 @id = ID, @name = Name FROM #Data -- 处理逻辑 DELETE FROM #Data WHERE ID = @idEND
-- 创建进度监控存储过程CREATE PROCEDURE usp_BatchProcess @TotalCount INT, @BatchSize INT = 1000ASBEGIN DECLARE @CurrentCount INT = 0 DECLARE @BatchCount INT = 0 DECLARE @StartTime DATETIME = GETDATE() WHILE @CurrentCount < @TotalCount BEGIN SET @BatchCount = @BatchCount + 1 -- 执行批量操作 -- ... 业务逻辑 ... SET @CurrentCount = @CurrentCount + @BatchSize -- 每10批输出一次进度 IF @BatchCount % 10 = 0 BEGIN DECLARE @Percent DECIMAL(5,2) = @CurrentCount * 100.0 / @TotalCount DECLARE @ElapsedSeconds INT = DATEDIFF(SECOND, @StartTime, GETDATE()) DECLARE @EstimateSeconds INT = @ElapsedSeconds * 100 / @Percent PRINT CONVERT(VARCHAR, GETDATE(), 120) + ' - 进度:' + CAST(@Percent AS VARCHAR) + '%' + ' - 已处理:' + CAST(@CurrentCount AS VARCHAR) + ' - 预计剩余:' + CAST(@EstimateSeconds - @ElapsedSeconds AS VARCHAR) + '秒' END END PRINT '处理完成,总耗时:' + CAST(DATEDIFF(SECOND, @StartTime, GETDATE()) AS VARCHAR) + '秒'END
需要循环处理? ↓是否可以用集合操作? → 是 → 使用UPDATE/INSERT/SELECT(最优) ↓ 否数据量是否<1000? → 是 → While循环可接受 ↓ 否是否需要分批处理(防日志爆满)? → 是 → While分批(推荐) ↓ 否是否需要复杂业务逻辑? → 是 → While循环(可接受) ↓ 否→ 重新设计,用集合操作
-- 完整示例:订单状态批量更新CREATE PROCEDURE usp_BatchUpdateOrderStatus @BatchSize INT = 5000, @MaxBatchCount INT = 100, @DelayMs INT = 100 -- 批次间延迟(毫秒)ASBEGIN SET NOCOUNT ON DECLARE @BatchCount INT = 0 DECLARE @ProcessedCount INT = 0 DECLARE @StartTime DATETIME = GETDATE() -- 创建进度表 CREATE TABLE #Progress ( BatchNo INT, ProcessedCount INT, BatchTime DATETIME, DurationMs INT ) WHILE @BatchCount < @MaxBatchCount BEGIN DECLARE @BatchStartTime DATETIME = GETDATE() SET @BatchCount = @BatchCount + 1 -- 分批更新 UPDATE TOP (@BatchSize) Orders SET Status = 1, ProcessDate = GETDATE() WHERE Status = 0 AND OrderDate < DATEADD(day, -30, GETDATE()) DECLARE @AffectedRows INT = @@ROWCOUNT SET @ProcessedCount = @ProcessedCount + @AffectedRows -- 记录进度 INSERT INTO #Progress (BatchNo, ProcessedCount, BatchTime, DurationMs) VALUES ( @BatchCount, @AffectedRows, GETDATE(), DATEDIFF(ms, @BatchStartTime, GETDATE()) ) -- 打印进度 PRINT CONVERT(VARCHAR, GETDATE(), 120) + ' - 第' + CAST(@BatchCount AS VARCHAR) + '批' + ' - 处理:' + CAST(@AffectedRows AS VARCHAR) + '条' + ' - 累计:' + CAST(@ProcessedCount AS VARCHAR) + '条' -- 如果没有更多数据,退出循环 IF @AffectedRows < @BatchSize BREAK -- 批次间延迟,减少系统压力 IF @DelayMs > 0 AND @BatchCount < @MaxBatchCount WAITFOR DELAY '00:00:00.' + RIGHT('000' + CAST(@DelayMs AS VARCHAR), 3) END -- 输出汇总信息 DECLARE @TotalDuration INT = DATEDIFF(second, @StartTime, GETDATE()) PRINT '' PRINT '========== 处理完成 ==========' PRINT '总批次数:' + CAST(@BatchCount AS VARCHAR) PRINT '总处理数:' + CAST(@ProcessedCount AS VARCHAR) PRINT '总耗时:' + CAST(@TotalDuration AS VARCHAR) + '秒' PRINT '平均每批:' + CAST(@TotalDuration * 1000 / @BatchCount AS VARCHAR) + '毫秒' -- 返回详细进度 SELECT BatchNo AS 批次, ProcessedCount AS 处理条数, BatchTime AS 处理时间, DurationMs AS 耗时毫秒 FROM #Progress ORDER BY BatchNo DROP TABLE #ProgressEND
-- 性能对比测试脚本CREATE PROCEDURE usp_CompareWhilePerformanceASBEGIN SET NOCOUNT ON -- 测试数据准备 IF OBJECT_ID('tempdb..#TestData') IS NOT NULL DROP TABLE #TestData CREATE TABLE #TestData (ID INT PRIMARY KEY, Value VARCHAR(100)) -- 插入10万条测试数据 ;WITH Numbers AS ( SELECT TOP 100000 ROW_NUMBER() OVER (ORDER BY a.object_id) AS n FROM sys.objects a, sys.objects b ) INSERT INTO #TestData (ID, Value) SELECT n, 'Value_' + CAST(n AS VARCHAR) FROM Numbers -- 测试1:逐行更新 PRINT '测试1:逐行更新' DECLARE @StartTime DATETIME = GETDATE() DECLARE @i INT = 1 WHILE @i <= 100000 BEGIN UPDATE #TestData SET Value = 'Updated_' + CAST(@i AS VARCHAR) WHERE ID = @i SET @i = @i + 1 END PRINT '逐行更新耗时:' + CAST(DATEDIFF(ms, @StartTime, GETDATE()) AS VARCHAR) + 'ms' -- 重置数据 UPDATE #TestData SET Value = 'Value_' + CAST(ID AS VARCHAR) -- 测试2:分批更新 PRINT '测试2:分批更新(每批1000条)' SET @StartTime = GETDATE() SET @i = 1 WHILE @i <= 100000 BEGIN UPDATE #TestData SET Value = 'Batch_' + CAST(@i AS VARCHAR) WHERE ID BETWEEN @i AND @i + 999 SET @i = @i + 1000 END PRINT '分批更新耗时:' + CAST(DATEDIFF(ms, @StartTime, GETDATE()) AS VARCHAR) + 'ms' -- 测试3:一次性更新 PRINT '测试3:一次性更新' SET @StartTime = GETDATE() UPDATE #TestData SET Value = 'Set_Updated' PRINT '一次性更新耗时:' + CAST(DATEDIFF(ms, @StartTime, GETDATE()) AS VARCHAR) + 'ms'END-- 执行测试EXEC usp_CompareWhilePerformance
优先考虑集合操作:能用一条SQL解决的,绝不用循环
分批处理大数据:使用WHILE + TOP/BETWEEN分批操作
添加进度监控:让运维人员了解执行进度
控制批次大小:根据数据量调整BatchSize(通常1000-10000)
添加延迟:WAITFOR避免长时间锁表
循环内使用游标:游标 + WHILE = 性能灾难
循环内使用SELECT*FROM:重复查询浪费资源
无限制循环:忘记更新计数器导致死循环
循环内使用DISTINCT/ORDER BY:每次都排序
忽略事务日志:大事务不分批导致日志暴涨
核心原则:能用集合不用循环,必须循环时控制批次和进度。
阅读原文:原文链接