Less Than Dot is a community of passionate IT professionals and enthusiasts dedicated to sharing technical knowledge, experience, and assistance. Inside you will find reference materials, interesting technical discussions, and expert tips and commentary. Once you register for an account you will have immediate access to the forums and all past articles and commentaries.
Cursors and How to Avoid Them
From Wiki
Row by row processing, the impact on performance
In order to avoid using cursors, the developer must first understand why it is important to do so. Cursors operate on one row at a time whereas SQL Server is optimized to perform set-based operations (those affecting all the rows in one operation). Therefore a set-based solution is usually significantly faster than a cursor solution. How much faster of course depends on many factors, the number of records, the complexity of the operation, the load on the server, etc. But in every case where I have ever been able to replace a cursor with a set-based solution, performance has improved significantly. One real life example, deleting 40,000 records from a table is shown below:
Reads Writes Time to process Type of Action 321704224 46355 47 Min 8 seconds With Cursor 1480203 31109 32 Seconds Without Cursor
I want to emphasize that the above example is typical of the performance gain by replacing a cursor. I’ve even seen times when a process was reduced from several hours to seconds or minutes. Once I eliminated a cursor from a process and the time was reduced from more than 24 hours to less than ten minutes. As you can see, cursors are performance hogs that should only be used if the data set affected is very tiny (and the cursor will not be used frequently by the application) or if there is no other realistic choice to accomplish the task. I also believe that even when the data set it small, it is usually worthwhile to learn to perform the task without a cursor, so that the methods to avoid the cursor are well-learned and available when you need to affect a large data set. This is particularly true for the developer who has not yet learned to think in set-based terms as his or her first choice.
Typical Uses of a Cursor and Set-Based Replacements for Them
To perform an insert using the values clause
Sometimes a cursor is used because the developer only knows how to insert using the VALUES clause. Since this syntax supports the insert of only one record at a time, a cursor is used if you have multiple records. However, it is better in this case to use the SELECT clause syntax instead of the VALUES clause.
For instance, the following code:
- DECLARE @Employee_Name VARCHAR (100), @Employee_Start_Date DATETIME
- DECLARE Employees_Cursor CURSOR
- FOR
- SELECT Employee_name, Employee_Start_Date FROM employees
- where Employee_Start_Date > Dateadd(d,-7, getdate())
- OPEN Employees_Cursor
- FETCH NEXT FROM Employees_Cursor INTO @Employee_Name, @Employee_Start_Date
- WHILE @@FETCH_STATUS = 0
- BEGIN
- INSERT New_Employees (New_Employee_name, New_Employee_Start_Date)
- VALUES (@Employee_Name, @Employee_Start_Date)
- FETCH NEXT FROM Employees_Cursor INTO @Employee_Name, @Employee_Start_Date
- END
- CLOSE Employees_Cursor
- DEALLOCATE Employees_Cursor
can be replaced with
- INSERT New_Employees (New_Employee_name, New_Employee_Start_Date)
- SELECT Employee_name, Employee_Start_Date FROM employees
- where Employee_Start_Date > Dateadd(d,-7, getdate())
Using joins in updates, and deletes
Sometimes developers use cursors because they don’t know the syntax for using joins in updates or deletes. So when they want to update or delete based on the values in a differ table they reach for a cursor. Using a join is not only faster, the syntax is actually simpler once you become familiar with it.
In the case of an update where you need to reference another table, the cursor solution might look like this:
- DECLARE @person_id INT
- DECLARE @field2 VARCHAR (10)
- DECLARE TestCursor3 CURSOR FOR
- SELECT t2.person_id, t2.field2 FROM table2 t2
- OPEN TestCursor3
- FETCH NEXT FROM TestCursor3
- INTO @person_id, @field2
- WHILE @@FETCH_STATUS = 0
- BEGIN
- UPDATE table1
- SET field2 = @field2
- WHERE person_id = @person_id
- FETCH NEXT FROM TestCursor3 INTO @person_id, @field2
- END
- CLOSE TestCursor3
- DEALLOCATE TestCursor3
It can be replaced by the following set-based solution:
- UPDATE t1
- SET field2 = t2.field2
- --SELECT t1.field2, t2, field2, t1.person_id
- FROM table1 t1
- JOIN table2 t2 ON t2.person_id = t1.person_id
Note: I have placed a commented out line that includes a select in this statement. Normally when I am creating this type of code, I will do this so that I can verify what will be updated before running the code while I am in the development process. This can save many hours of fixing a bad update because you didn’t realize it would update 1,000,000 rows when you thought that only about 1,000 should have been updated. I never run an update or delete statement for the first time that contains a join or complex where clause without first checking the select for the same code. If I am going to do something that impacts the data in the database, I need to be sure it will impact the correct data before I run it against production data.
Now suppose I wanted to delete any records from one table where there is no corresponding record in a different table. If I use a cursor, the code might look something like this:
- DECLARE @person_id INT
- DECLARE TestCursor4 CURSOR FOR
- SELECT t1.person_id FROM table1 t1
- JOIN table2 t2 ON t2.person_id = t1.person_id
- WHERE t2.field1 IS NULL
- OPEN TestCursor4
- FETCH NEXT FROM TestCursor4
- INTO @person_id
- WHILE @@FETCH_STATUS = 0
- BEGIN
- DELETE table1
- WHERE person_id = @person_id
- FETCH NEXT FROM TestCursor4
- INTO @person_id
- END
- CLOSE TestCursor4
- DEALLOCATE TestCursor4
This can be replaced by the following set-based solution:
- DELETE T1
- --SELECT1.*
- FROM table1 T1
- JOIN table2 T2 ONT2.person_id =T1.person_id
- WHERE T2.field1 IS NULL
The need to process in different tables depending on a value in the table
- DECLARE @person_id INT
- DECLARE @field2 VARCHAR (10)
- DECLARE TestCursor6 CURSOR FOR
- SELECT T2.person_id, T2.field2 FROM table2 T2
- OPEN TestCursor6
- FETCH NEXT FROM TestCursor6
- INTO @person_id, @field2
- WHILE @@FETCH_STATUS = 0
- BEGIN
- IF @field2 = 'test'
- BEGIN
- UPDATE table1
- SET field2 = @field2
- WHERE person_id = @person_id
- END
- ELSE
- UPDATE table3
- SET field2 = @field2
- WHERE person_id = @person_id
- FETCH NEXT FROM TestCursor6
- INTO @person_id, @field2
- END
- CLOSE TestCursor6
- DEALLOCATE TestCursor6
You can replace the cursor with
- UPDATE T1
- SET field2 =T2.field2
- --SELECT T1.field2, T2.field2, T1.person_id
- FROM table1 T1
- JOIN table2 T2 ON T2.person_id = T1.person_id
- WHERE field2 = 'test'
- UPDATE T3
- SET field2 = t2.field2
- --SELECT T3.field2,T2, field2, T3.person_id
- FROM table3 T3
- JOIN table2 Tt2 ON T2.person_id = T3.person_id
- WHERE field2 <> 'test'
Using case for special treatment based on a the data in a field
Sometimes developers don’t realize that they can use a CASE statement to perform processing based on the data in the field. So they write a cursor to look at each value and go to a particular branch of the code depending on the value.
- DECLARE @Employee_ID INT, @Employee_Status varchar (100)
- DECLARE Employees_Cursor CURSOR
- FOR
- SELECT Employee_ID, Employee_Status FROM Employees
- OPEN Employees_Cursor
- FETCH NEXT FROM Employees_Cursor INTO @Employee_ID, @Employee_Status
- WHILE @@FETCH_STATUS = 0
- BEGIN
- IF @Employee_Status = 'new hire probation'
- UPDATE Employee_groups
- SET Group_id = 2
- where Employee_ID = @Employee_ID
- ELSE IF @Employee_Status = 'performance probation'
- UPDATE Employee_groups
- SET Group_id = 2
- where Employee_ID = @Employee_ID
- ELSE
- UPDATE Employee_groups
- SET Group_ID= 1
- where Employee_ID = @Employee_ID
- FETCH NEXT FROM Employees_Cursor INTO @Employee_ID, @Employee_Status
- END
- CLOSE Employees_Cursor
- DEALLOCATE Employees_Cursor
Replace the cursor with a set-based solution using the CASE statement
- UPDATE EG
- SET Group_ID = CASE
- WHEN E.Employee_Status = ‘new hire probation’ THEN 2
- WHEN E.Employee_Status = ‘performance probation’ THEN 3
- ELSE 1 END
- FROM Employee_groups EG
- JOIN Employees E on E.Employee_ID = EG.EmployeeID
The need to do either an update or an insert depending on whether the record exists in the table
Another time people use cursors is when they don’t know if a record is already in a table. So they look at each record individually to decide to insert or update.
- DECLARE @Employee_ID INT
- DECLARE Employees_Cursor CURSOR
- FOR
- SELECT Employee_ID FROM Employees
- OPEN Employees_Cursor
- FETCH NEXT FROM Employees_Cursor INTO @Employee_ID
- WHILE @@FETCH_STATUS = 0
- BEGIN
- IF EXISTS(SELECT 1 FROM EmployeeAddress WHERE Employee_ID = @Employee_ID)
- UPDATE EA
- SET Street = EI.Street,
- City = EI.city,
- [State] = EI.[State]
- from EmployeeAddress EA
- JOIN EmployeeImport EI on EA.Employee_id = EI.Employee_id
- where EA.Employee_ID = @Employee_ID
- ELSE
- INSERT EmployeeAddress (Employee_id, Street, City, [State])
- select Employee_id, Street, City, [State] from EmployeeImport EI
- where EI.Employee_ID = @Employee_ID
- FETCH NEXT FROM Employees_Cursor INTO @Employee_ID
- END
- CLOSE Employees_Cursor
- DEALLOCATE Employees_Cursor
This can be replaced with a MERGE query
- MERGE dbo.EmployeeAddress AS Target
- USING (select Employee_id, Street, City, [State] from EmployeeImport EI)
- AS Source
- ON (Target.Employee_id = Source.Employee_id )
- WHEN MATCHED THEN
- UPDATE
- SET Target.Street = Source.Street,
- Target.City = Source.city,
- Target.[State] =Source.[State]
- WHEN NOT MATCHED BY TARGET THEN
- INSERT(Employee_id, Street, City, [State])
- VALUES( Source.Employee_id, Source.Street, Source.City, Source.[State])
See also:
The Truth About Cursors: Part I



LTD Social Sitings
Note: Watch for social icons on posts by your favorite authors to follow their postings on these and other social sites.