Introduction
They say not to use Cursors. They are right and wrong at the same time.
If Cursors are bad, why is it not removed from SQL?
Background
Cursors are probably slow in performance concerning normal code (Sets). Therefore, we avoid using them. But at the same time, it is unavoidable and is proffered to be used in cases where there is a need to prepare dynamic SQL or complex logics row by row.
The article primarily focuses on determining a boundary between the two, Cursors and Sets.
Explanation
Cursor in SQL
If the developers have worked with Visual Basic (VB) precisely in recordsets, it works similarly to that of a cursor.
The cursor iterates through every single row for processing, and each time it fetches a row, it makes a network round trip. Since it makes the round trips, the network bandwidth would go for a toss, and repeatedly doing this can directly impact the operation used with the Cursor.
The following is a simple description of how cursors are used in SQL procedures:
- Declare a cursor that defines a result set.
- Open the cursor to establish the result set.
- Fetch the data into local variables from the cursor, one row at a time.
- Close the cursor when done.
Here is the sample code of a cursor:
DECLARE cust_cursor CURSOR
FOR SELECT * FROM Cutomers
OPEN cust_cursor
FETCH NEXT FROM cust_cursor;
CLOSE cust_cursor
Sets in SQL
The fundamental approach of SQL is to differentiate a pool of data logically. SQL works on sets, in other words, with a set of records. Therefore, Sets can replace the cursor to a maximum level. These are normal SQL queries. The following example shows the difference between them.
Example
Problem. Update all the records in the Customer table with a respective pincode using the table Pincode_Details.
Solution using the Cursor
Here is what the code says.
- Fetch the records (Telephone numbers) with pincode null from the table Customer.
- Iterate to every fetched record and break the preceding 4 digits of the telephone number.
- Find the respective pincode from Pincode_details using the number fetched in Step 2.
- For every record, check if the variable @pincode is not null and update the pincode into the Customer table.
DECLARE @telnumber char(8)
DECLARE cust_cursor CURSOR FOR //
SELECT TelNumber FROM Customer WHERE PinCode IS NULL //
OPEN cust_cursor //
FETCH NEXT FROM cust_cursor INTO @telnumber // (1)
WHILE @@FETCH_STATUS = 0 BEGIN
Declare @pincode char(6)
DECLARE @centerid char(4)
SELECT @centerid = LEFT(@telnumber, 4) // (2)
SELECT @pincode = PinCode //
FROM PinCode_Details //
WHERE Centerid = @centerid // (3)
IF @pincode IS NOT NULL //
BEGIN //
UPDATE Customer SET PinCode = @pinCode //
WHERE CURRENT OF cust_cursor //
END // (4)
FETCH NEXT FROM cust_cursor INTO @telnumber
END
CLOSE cust_cursor
DEALLOCATE cust_cursor
Solution using the Sets
A single update query with a join will achieve the same result.
UPDATE Customer
SET PinCode = PinCode_Details.PinCode
FROM Customer
JOIN PinCode_Details ON
LEFT(Customer.PhoneNumber, 4) = PinCode_Details.Centerid
WHERE
Customer.PinCode IS NULL
Advantage of Sets over Cursor in the above example
- Sets are recommended since there will be a noticeable improvement in the query results.
- The code is easily readable and understandable.
- There will be no network round trips.
- The query can be optimized with indexing.
When can we use cursors?
Sets are only for use where the developer builds the query with prior knowledge of what the user will see or use.
- Cursors can be used when the user is given an interface to group the data logically. Then, the developer would have no idea of what kind of grouping will be done by the user.
- Or, as in the following example, if an event must be fired to update a table present in all the databases ('ClientProductionOne,' 'ClientProductionTwo,' 'ClientProductionThree'), then a cursor approach will help you.
DECLARE @dbname VARCHAR(50)
DECLARE @databasename VARCHAR(256)
DECLARE @SQL varchar(8000)
SET @SQL = 'UPDATE @dbname.dbo.tbldailyEventFired
SET EndTime = CONVERT(datetime,''2014-11-18 23:59:00'',120)
WHERE EndTime = (CONVERT(datetime,''2015-11-17 23:59:00'',120))'
DECLARE db_cursor CURSOR FOR
SELECT name
FROM master.dbo.sysdatabases
WHERE name IN ('ClientProductionOne','ClientProductionTwo','ClientProductionThree')
OPEN db_cursor
FETCH NEXT FROM db_cursor INTO @dbname
WHILE (@@FETCH_STATUS = 0)
BEGIN
SET @SQL = REPLACE(@SQL, '@dbname', @dbname)
PRINT @SQL
-- EXEC (@SQL)
FETCH NEXT FROM db_cursor INTO @dbname
END
CLOSE db_cursor
DEALLOCATE db_cursor
GO
Summary
In this article, we have tried to help the developers determine the approach (on choosing Cursor or Sets) which is probably better. And to point out the right choice of using the same.
Thanks to all my distinguished seniors/colleagues of my career for passing on their views and approaches when using Cursors and Sets.