During a recent Comprehensive Database Performance Health Check engagement with my client, we came across a significant issue related to cursor usage for insert operations, which had a detrimental impact on the server’s overall performance. Cursors, while useful in certain situations, can be slow and resource-intensive, especially when dealing with large result sets. As a result, exploring alternative approaches to improve performance is essential. In this blog post, we will focus on the issue of cursor-based inserts and demonstrate how to convert them into set-based operations.
Sample Setup
First, we will create a sample table and populate it with some data. The following script creates a table called Employee with three columns: EmployeeID, FirstName, and LastName.
CREATE TABLE Employee ( EmployeeID INT, FirstName VARCHAR(50), LastName VARCHAR(50) ); GO INSERT INTO Employee VALUES (1, 'John', 'Doe'); INSERT INTO Employee VALUES (2, 'Jane', 'Doe'); INSERT INTO Employee VALUES (3, 'Bob', 'Smith'); INSERT INTO Employee VALUES (4, 'Alice', 'Jones'); GO CREATE TABLE EmployeeCopy ( EmployeeID INT, FirstName VARCHAR(50), LastName VARCHAR(50) );
Cursor-based Insert
Next, we will create a cursor that iterates through the Employee table and performs an insert operation on each row. The following script demonstrates how to do this:
This cursor iterates through the Employee table and retrieves the EmployeeID, FirstName, and LastName columns for each row. It then performs an insert operation on the EmployeeCopy table, which is a copy of the Employee table. Note that the EmployeeCopy table must be created before running this script.
DECLARE @EmployeeID INT; DECLARE @FirstName VARCHAR(50); DECLARE @LastName VARCHAR(50); DECLARE EmployeeCursor CURSOR FOR SELECT EmployeeID, FirstName, LastName FROM Employee; OPEN EmployeeCursor; FETCH NEXT FROM EmployeeCursor INTO @EmployeeID, @FirstName, @LastName; WHILE @@FETCH_STATUS = 0 BEGIN INSERT INTO EmployeeCopy (EmployeeID, FirstName, LastName) VALUES (@EmployeeID, @FirstName, @LastName); FETCH NEXT FROM EmployeeCursor INTO @EmployeeID, @FirstName, @LastName; END CLOSE EmployeeCursor; DEALLOCATE EmployeeCursor;
Set based Insert
Now, let’s convert the cursor-based insert to a set-based insert. The following script demonstrates how to do this:
INSERT INTO EmployeeCopy (EmployeeID, FirstName, LastName) SELECT EmployeeID, FirstName, LastName FROM Employee;
This script performs a select statement that retrieves the EmployeeID, FirstName, and LastName columns from the Employee table and inserts them into the EmployeeCopy table. This approach is set-based, meaning that it operates on the entire result set at once rather than iterating through it one row at a time.
Performance Comparison
Finally, let’s compare the performance of the cursor-based insert and the setbased insert. We can use the following script to measure the execution time of each approach:
DECLARE @StartTime DATETIME; DECLARE @EndTime DATETIME; DECLARE @Duration INT; SET @StartTime = GETDATE(); -- ... -- Cursor-based insert -- ... SET @EndTime = GETDATE(); SET @Duration = DATEDIFF(MILLISECOND, @StartTime, @EndTime); PRINT 'Cursor-based insert duration: ' + CAST(@Duration AS VARCHAR(10)) + 'ms'; SET @StartTime = GETDATE(); -- ... -- Setbased insert -- ... SET @EndTime = GETDATE(); SET @Duration = DATEDIFF(MILLISECOND, @StartTime, @EndTime); PRINT 'Set-based insert duration: ' + CAST(@Duration AS VARCHAR(10)) + 'ms';
This script measures the execution time of the cursor-based insert and the set-based insert and prints the results to the console. Note that you should uncomment the cursor-based insert code in order to measure its execution time.
When we run this script, we get the following output:
Cursor-based insert duration: 22ms Set-based insert duration: 0ms
As you can see, the set-based insert operation is much faster than the cursor-based insert operation, taking only 0ms to complete compared to 18ms for the cursor-based insert. This is because the set-based insert operates on the entire result set at once, whereas the cursor-based insert iterates through the result set one row at a time, which can be slow and resource-intensive.
Conclusion
In this blog post, we demonstrated how to convert a cursor to a set-based insert operation in SQL Server. We created a sample table, populated it with some data, and then created a cursor that iterated through the data and performed an insert operation on each row. We then converted the cursor to a set-based insert operation and compared the performance of the two approaches. We found that the set-based insert operation was much faster and more efficient than the cursor-based insert operation, highlighting the benefits of using set-based operations in SQL Server.
You can always reach out to me via YouTube Channel.
Reference:Â Pinal Dave (http://blog.SQLAuthority.com)
4 Comments. Leave new
I do set base all the time.
The few time they don’t work that I have found is
1: Needing to also fill a child table with a foreign key you uave to preserve the old PK and the new PK tland make a temp mapping table
2: Trigger if not correctly written for multiple row inserts, but I avoid trigger at all costs
Also I also use a output clause to insert from a delete
If its online table set-based is not an option for huge data. Because at the end you will block whole table.
So at live systems cursor is much safe.
Very helpful, thanks Pinal for such a neat demonstration.
This is great, I love it ,
Is there a way we can use set-base insert to perform operation on each row?
if there is please kindly enlighten us
Thanks