Microsoft SQL Server 2000 Programming by Example
482
FROM Categories
ORDER BY CategoryName ASC
Open the cursor
OPEN MyCategories
Fetch the first row
FETCH NEXT FROM MyCategories
Close the cursor
CLOSE MyCategories
Deallocate the cursor
DEALLOCATE MyCategories
Cursor created was not of the requested type.
CategoryID CategoryName Description
1 Beverages Soft drinks, coffees, teas, beers, and ales
The cursor must be defined for a SELECT statement. This is a normal SELECT statement with a few
exceptions. You cannot use COMPUTE, COMPUTE BY, FOR BROWSE, or INTO in a SELECT statement that
defines a cursor.
Caution
If the SELECT statement produces a result set that is not updatable, the cursor will be READ_ONLY.
This can happen because of the use of aggregate functions, insufficient permissions, or retrieving
read-only data.
You can restrict the columns to update inside the cursor using the FOR UPDATE clause, as shown in Listing
12.15. This clause can be used in two ways:
• FOR UPDATE OF Column1, , ColumnN—Use this option to define columns Column1 to
ColumnN as updatable through the cursor.
• FOR UPDATE—This is the default option, and it declares all the cursor columns as updatable.
Listing 12.15 Using the FOR UPDATE Clause
Chapter 12. Row-Oriented Processing: Using Cursors
483
DECLARE MyCategories CURSOR
KEYSET
FOR
SELECT CategoryID, CategoryName, Description
FROM Categories
ORDER BY CategoryName ASC
FOR UPDATE OF CategoryName, Description
Note
When you declare a cursor, SQL Server creates some memory structures to use the cursor, but the
data is not retrieved until you open the cursor.
Opening Cursors
To use a cursor, you must open it. You can open a cursor using the OPEN statement. If the cursor was
declared as STATIC or KEYSET, SQL Server must create a worktable in TempDB to store either the full result
set, in a STATIC cursor, or the keyset only in a keyset-driven cursor. In these cases, if the worktable cannot
be created for any reason, the OPEN statement will fail.
SQL Server can optimize the opening of big cursors by populating the cursor asynchronously. In this case,
SQL Server creates a new thread to populate the worktable in parallel, returning the control to the application
as soon as possible.
You can use the @@CURSOR_ROWS system function to control how many rows are contained in the cursor. If
the cursor is using asynchronous population, the value returned by @@CURSOR_ROWS will be negative and
represents the approximate number of rows returned since the opening of the cursor.
For dynamic cursors, @@CURSOR_ROWS returns -1, because it is not possible to know whether the full result
set has been returned already, because of potential insertions by other operations affecting the same data.
Caution
The @@CURSOR_ROWS function returns the number of rows of the last cursor opened in the current
connection. If you use cursors inside triggers, the result of this function from the main execution
level could be misleading. Listing 12.16 shows an example of this problem.
To specify when SQL Server will decide to populate a cursor asynchronously, you can use the
sp_configure system-stored procedure to change the server setting "cursor threshold", specifying
the maximum number of rows that will be executed directly without asynchronous population.
Caution
Microsoft SQL Server 2000 Programming by Example
484
Do not fix the "cursor threshold" value too low, because small result sets are more efficiently
opened synchronously.
Listing 12.16 Using the @@CURSOR_ROWS System Function
Create a procedure to open
a cursor on Categories
CREATE PROCEDURE GetCategories
AS
DECLARE MyCategories CURSOR STATIC
FOR
SELECT CategoryID, CategoryName
FROM Categories
OPEN MyCategories
Shows the number of rows in the cursor
SELECT @@CURSOR_ROWS 'Categories cursor rows after open'
CLOSE MyCategories
DEALLOCATE MyCategories
GO
Create a cursor on Products
DECLARE MyProducts CURSOR STATIC
FOR
SELECT ProductID, ProductName
FROM Products
OPEN MyProducts
Shows the number of rows in the last opened cursor,
which is MyProducts
SELECT @@CURSOR_ROWS 'Products cursor rows'
EXEC GetCategories
Shows the number of rows in the last opened cursor
in the current connection, which is MyCategories
SELECT @@CURSOR_ROWS 'Categories cursor rows after close and deallocated'
CLOSE MyProducts
Chapter 12. Row-Oriented Processing: Using Cursors
485
DEALLOCATE MyProducts
Products cursor rows
77
Categories cursor rows after open
8
Categories cursor rows after close and deallocated
0
Fetching Rows
You can use the FETCH statement to navigate an open cursor, as shown in Listing 12.17. Every time you
execute the FETCH statement, the cursor moves to a different row.
FETCH FROM CursorName retrieves the next row in the cursor. This is a synonym of FETCH NEXT FROM
CursorName. If the FETCH statement is executed right after the OPEN statement, the cursor is positioned in
the first row. If the current row is the last one in the result set, executing FETCH NEXT again will send the
cursor beyond the end of the result set and will return an empty row, but no error message will be produced.
Caution
After opening a cursor with the OPEN statement, the cursor does not point to any specific row, so
you must execute a FETCH statement to position the cursor in a valid row.
FETCH PRIOR moves the cursor to the preceding row. If the cursor was positioned already at the beginning of
the result set, using FETCH PRIOR will move the pointer before the starting of the result set, retrieving an
empty row, but no error message will be produced.
FETCH FIRST moves the cursor pointer to the beginning of the result set, returning the first row.
FETCH LAST moves the cursor pointer to the end of the result set, returning the last row.
FETCH ABSOLUTE n moves the cursor pointer to the n row in the result set. If n is negative, the cursor
pointer is moved n rows before the end of the result set. If the new row position does not exist, an empty row
will be returned and no error will be produced. If n is 0, no rows are returned and the cursor pointer goes out
of scope.
Microsoft SQL Server 2000 Programming by Example
486
FETCH RELATIVE n moves the cursor pointer n rows forward from the current position of the cursor. If n is
negative, the cursor pointer is moved backward n rows from the current position. If the new row position does
not exist, an empty row will be returned and no error will be produced. If n is 0, the current row is returned.
You can use the @@FETCH_STATUS system function to test whether the cursor points to a valid row after the
last FETCH statement. @@FETCH_SATUS can have the following values:
• 0 if the FETCH statement was successful and the cursor points to a valid row.
• -1 if the FETCH statement was not successful or the cursor points beyond the limits of the result set.
This can be produced using FETCH NEXT from the last row or FETCH PRIOR from the first row.
• -2 the cursor is pointing to a nonexistent row. This can be produced by a keyset-driven cursor when
one of the rows has been deleted from outside the control of the cursor.
Caution
@@FETCH_STATUS is global to the connection, so it reflects the status of the latest FETCH
statement executed in the connection. That is why it is important to test it right after the FETCH
statement.
Listing 12.17 Use FETCH to Navigate the Cursor
DECLARE MyProducts CURSOR STATIC
FOR
SELECT ProductID, ProductName
FROM Products
ORDER BY ProductID ASC
OPEN MyProducts
SELECT @@CURSOR_ROWS 'Products cursor rows'
SELECT @@FETCH_STATUS 'Fetch Status After OPEN'
FETCH FROM Myproducts
SELECT @@FETCH_STATUS 'Fetch Status After first FETCH'
FETCH NEXT FROM MyProducts
SELECT @@FETCH_STATUS 'Fetch Status After FETCH NEXT'
FETCH PRIOR FROM Myproducts
SELECT @@FETCH_STATUS 'Fetch Status After FETCH PRIOR'
Chapter 12. Row-Oriented Processing: Using Cursors
487
FETCH PRIOR FROM Myproducts
SELECT @@FETCH_STATUS 'Fetch Status After FETCH PRIOR the first row'
FETCH LAST FROM Myproducts
SELECT @@FETCH_STATUS 'Fetch Status After FETCH LAST'
FETCH NEXT FROM Myproducts
SELECT @@FETCH_STATUS 'Fetch Status After FETCH NEXT the last row'
FETCH ABSOLUTE 10 FROM Myproducts
SELECT @@FETCH_STATUS 'Fetch Status After FETCH ABSOLUTE 10'
FETCH ABSOLUTE -5 FROM Myproducts
SELECT @@FETCH_STATUS 'Fetch Status After FETCH ABSOLUTE -5'
FETCH RELATIVE -20 FROM Myproducts
SELECT @@FETCH_STATUS 'Fetch Status After FETCH RELATIVE -20'
FETCH RELATIVE 10 FROM Myproducts
SELECT @@FETCH_STATUS 'Fetch Status After FETCH RELATIVE 10'
CLOSE MyProducts
SELECT @@FETCH_STATUS 'Fetch Status After CLOSE'
DEALLOCATE MyProducts
Products cursor rows
77
Fetch Status After OPEN
0
ProductID ProductName
1 Chai
Fetch Status After first FETCH
0
ProductID ProductName
Microsoft SQL Server 2000 Programming by Example
488
2 Chang
Fetch Status After FETCH NEXT
0
ProductID ProductName
1 Chai
Fetch Status After FETCH PRIOR
0
ProductID ProductName
Fetch Status After FETCH PRIOR the first row
-1
ProductID ProductName
77 Original Frankfurter grüne Soße
Fetch Status After FETCH LAST
0
ProductID ProductName
Fetch Status After FETCH NEXT the last row
-1
ProductID ProductName
10 Ikura
Fetch Status After FETCH ABSOLUTE 10
0
ProductID ProductName
73 Röd Kaviar
Fetch Status After FETCH ABSOLUTE -5
0
ProductID ProductName
53 Perth Pasties
Fetch Status After FETCH RELATIVE -20
0
Chapter 12. Row-Oriented Processing: Using Cursors
489
ProductID ProductName
63 Vegie-spread
Fetch Status After FETCH RELATIVE 10
0
Fetch Status After CLOSE
0
At the same time you are moving the cursor with the FETCH statement, you can use the INTO clause to
retrieve the cursor fields directly into user-defined variables (see Listing 12.18). In this way, you later can
use the values stored in these variables in further Transact-SQL statements.
Listing 12.18 Use FETCH INTO to Get the Values of the Cursor Columns into Variables
SET NOCOUNT ON
GO
DECLARE @ProductID int,
@ProductName nvarchar(40),
@CategoryID int
DECLARE MyProducts CURSOR STATIC
FOR
SELECT ProductID, ProductName, CategoryID
FROM Products
WHERE CategoryID BETWEEN 6 AND 8
ORDER BY ProductID ASC
OPEN MyProducts
FETCH FROM Myproducts
INTO @ProductID, @ProductName, @CategoryID
WHILE @@FETCH_STATUS = 0
BEGIN
SELECT @ProductName as 'Product',
CategoryName AS 'Category'
FROM Categories
WHERE CategoryID = @CategoryID
FETCH FROM Myproducts
INTO @ProductID, @ProductName, @CategoryID
END
CLOSE MyProducts
DEALLOCATE MyProducts
Microsoft SQL Server 2000 Programming by Example
490
Product Category
Uncle Bob's Organic Dried Pears Produce
Product Category
Mishi Kobe Niku Meat/Poultry
Product Category
Ikura Seafood
Product Category
Konbu Seafood
Product Category
Tofu Produce
Product Category
Alice Mutton Meat/Poultry
Product Category
Carnarvon Tigers Seafood
Product Category
Rössle Sauerkraut Produce
Product Category
Thüringer Rostbratwurst Meat/Poultry
Product Category
Nord-Ost Matjeshering Seafood
Product Category
Inlagd Sill Seafood
Product Category
Gravad lax Seafood
Product Category
Boston Crab Meat Seafood
Product Category
Chapter 12. Row-Oriented Processing: Using Cursors
491
Jack's New England Clam Chowder Seafood
Product Category
Rogede sild Seafood
Product Category
Spegesild Seafood
Product Category
Manjimup Dried Apples Produce
Product Category
Perth Pasties Meat/Poultry
Product Category
Tourti\'e8re Meat/Poultry
Product Category
Pâté chinois Meat/Poultry
Product Category
Escargots de Bourgogne Seafood
Product Category
Röd Kaviar Seafood
Product Category
Longlife Tofu Produce
If the cursor is updatable, you can modify values in the underlying tables sending standard UPDATE or
DELETE statements and specifying WHERE CURRENT OF CursorName as a restricting condition (see
Listing 12.19).
Listing 12.19 Using WHERE CURRENT OFto Apply Modifications to the Current Cursor Row
BEGIN TRAN
Declare the cursor
DECLARE MyProducts CURSOR
FORWARD_ONLY
FOR
Microsoft SQL Server 2000 Programming by Example
492
SELECT ProductID, ProductName
FROM Products
WHERE ProductID > 70
ORDER BY ProductID
Open the cursor
OPEN MyProducts
Fetch the first row
FETCH NEXT FROM MyProducts
UPdate the name of the product
and the UnitPrice in the current cursor position
update Products
set ProductName = ProductName + '(to be dicontinued)',
UnitPrice = UnitPrice * (1.0 + CategoryID / 100.0)
where current of MyProducts
SELECT *
from Products
Close the cursor
CLOSE MyProducts
Deallocate the cursor
DEALLOCATE MyProducts
ROLLBACK TRAN
Note
You can update through cursor columns that are not part of the cursor definition, as long as the
columns are updatable
Closing Cursors
Use the CLOSE statement to close a cursor, freeing any locks used by it. The cursor structure is not destroyed,
but it is not possible to retrieve any data from the cursor after the cursor is closed.
Tip
It is a good practice to close cursors as soon as they are not necessary. This simple practice can
provide better concurrency to your application.
Most of the listings in this chapter use the CLOSE statement.
Chapter 12. Row-Oriented Processing: Using Cursors
493
Deallocating Cursors
To destroy the cursor completely, you can use the DEALLOCATE statement. After this statement is executed, it
is not possible to reopen the cursor without redefining it again.
After DEALLOCATE you can reuse the cursor name to declare any other cursor, with identical or different
definition.
Tip
To reuse the same cursor in different occasions in a long batch or a complex stored procedure, you
should declare the cursor as soon as you need it and deallocate it when it is no longer necessary.
Between the DECLARE and DEALLOCATE statements, use OPEN and CLOSE to access data as
many times as necessary to avoid long-standing locks. However, consider that each time you open
the cursor the query has to be executed. This could produce some overhead.
Scope of Cursors
In the DECLARE CURSOR statement, you can specify the scope of the cursor after its name. The default scope
is GLOBAL, but you can change the default scope, changing the database option default to local cursor.
Caution
You should not rely on the default cursor scope of SQL Server. It is recommended that you declare
the cursor explicitly as either LOCAL or GLOBAL, because the default cursor scope might change in
future versions of SQL Server.
You can use a global cursor anywhere in the same connection in which the cursor was created, whereas local
cursors are valid only within the scope of the batch, procedure, user-defined function, or trigger where the
cursor is created. The cursor is automatically deallocated when it goes out of scope (see Listing 12.20).
Listing 12.20 Using Global Cursors
Declare the cursor as GLOBAL
DECLARE MyProducts CURSOR GLOBAL
FOR
SELECT ProductID, ProductName
FROM Products
WHERE ProductID > 70
ORDER BY ProductID
However, you can assign the cursor to an OUTPUT parameter in a stored procedure. In this case, the cursor
will be deallocated when the last cursor variable that references the cursor goes out of scope.
Note
Microsoft SQL Server 2000 Programming by Example
494
Cursor variables are covered later in this chapter.
Global and local cursors have two different name spaces, so it is possible to have a global cursor with the
same name as a local cursor, and they can have completely different definitions. To avoid potential problems,
SQL Server use local cursors.
Local Cursors
Local cursors are a safety feature that provides the creation of local cursors inside independent objects, such
as stored procedures, triggers, and user-defined functions. Local cursors are easier to manage than global
cursors because you do not have to consider potential changes to the cursor in other procedures or triggers
used by your application.
Global Cursors
Global cursors are useful in scenarios where different procedures must manage a common result set, and
they must dynamically interact with it. It is recommended you use local cursors whenever possible. If you
require sharing a cursor between two procedures, consider using a cursor variable instead, as is covered in
the next section.
Using Cursor Variables
It is possible to declare variables using the cursor data type, which is very useful if you need to send a
reference of your cursor to another procedure or user-defined function. Using cursor variables is similar to
using standard cursors (see Listing 12.21).
Listing 12.21 Using Cursor Variables
Declare the cursor variable
DECLARE @Products AS CURSOR
Assign the cursor variable a cursor definition
SET @Products = CURSOR STATIC
FOR
SELECT ProductID, ProductName
FROM Products
Open the cursor
OPEN @Products
Fetch the first cursor row
Chapter 12. Row-Oriented Processing: Using Cursors
495
FETCH NEXT FROM @Products
Close the cursor
CLOSE @Products
Deallocate the cursor
DEALLOCATE @Products
SQL Server provides system stored procedures to retrieve information about cursors. These procedures use
cursor variables to communicate its data:
• sp_cursor_list produces a list of available cursors in the current connection.
• sp_describe_cursor retrieves the attributes of an open cursor. The out put is the same as the
output produced with sp_cursor_list, but sp_describe_cursor refers to a single cursor.
• sp_describe_cursor_columns describes the columns retrieved by the cursor.
• sp_describe_cursor_tables gets information about the tables used in the cursor.
These stored procedures use cursor variables to retrieve results. In this way, calling procedures and batches
can use the result one row at a time.
Listing 12.22 shows how to execute these system stored procedures to get information about cursors and
cursors variables.
Listing 12.22 Retrieving Information About Cursors with System Stored Procedures
USE Northwind
GO
Declare some cursors
DECLARE CCategories CURSOR LOCAL
DYNAMIC
FOR
SELECT CategoryName
FROM Categories
DECLARE CCustomers CURSOR LOCAL
FAST_FORWARD
FOR
SELECT CompanyName
FROM Customers
DECLARE COrdersComplete CURSOR GLOBAL
KEYSET
FOR
SELECT O.OrderID, OrderDate,
C.CustomerID, CompanyName,
P.ProductID, ProductName,
Quantity, OD.UnitPrice, Discount
FROM Orders O
JOIN [Order Details] OD
Microsoft SQL Server 2000 Programming by Example
496
ON OD.OrderID = O.OrderID
JOIN Customers C
ON C.CustomerID = O.CustomerID
JOIN Products P
ON P.ProductID = OD.ProductID
Declare a cursor variable to hold
results from the stored procedures
DECLARE @OutputCursor AS CURSOR
Get information about declared local cursors
EXEC sp_cursor_list @OutputCursor OUTPUT, 1
deallocate the cursor, so we can reuse the cursor variable
DEALLOCATE @OutputCursor
Or get information about declared global cursors
EXEC sp_cursor_list @OutputCursor OUTPUT, 2
deallocate the cursor, so we can reuse the cursor variable
DEALLOCATE @OutputCursor
Or get information about declared global and local cursors
note that status = -1 means cursor closed
PRINT CHAR(10) + 'sp_cursor_list cursor OUTPUT'+ CHAR(10)
EXEC sp_cursor_list @OutputCursor OUTPUT, 3
FETCH NEXT FROM @OutputCursor
WHILE @@FETCH_STATUS = 0
FETCH NEXT FROM @OutputCursor
deallocate the cursor, so we can reuse the cursor variable
DEALLOCATE @OutputCursor
Open the CCategories cursor
OPEN CCategories
Get information about a cursor
note that status = 1 means cursor open
EXEC sp_describe_cursor @OutputCursor OUTPUT,
N'local', N'CCategories'
PRINT CHAR(10) + 'sp_describe_cursor cursor OUTPUT'+ CHAR(10)
FETCH NEXT FROM @OutputCursor
WHILE @@FETCH_STATUS = 0
FETCH NEXT FROM @OutputCursor
deallocate the cursor, so we can reuse the cursor
variable
Chapter 12. Row-Oriented Processing: Using Cursors
497
DEALLOCATE @OutputCursor
CLOSE CCategories
Open the CCustomers cursor
OPEN CCustomers
Get information about a cursor
note that status = 1 means cursor open
EXEC sp_describe_cursor_columns @OutputCursor OUTPUT,
N'local', N'CCustomers'
PRINT CHAR(10) + 'sp_describe_cursor_columns cursor OUTPUT'+ CHAR(10)
FETCH NEXT FROM @OutputCursor
WHILE @@FETCH_STATUS = 0
FETCH NEXT FROM @OutputCursor
deallocate the cursor, so we can reuse the cursor variable
DEALLOCATE @OutputCursor
CLOSE CCustomers
Open the CCategories cursor
OPEN COrdersComplete
Get information about a cursor
note that status = 1 means cursor open
EXEC sp_describe_cursor_tables @OutputCursor OUTPUT,
N'global', N'COrdersComplete'
PRINT CHAR(10) + 'sp_describe_cursor_tables cursor OUTPUT'+ CHAR(10)
FETCH NEXT FROM @OutputCursor
WHILE @@FETCH_STATUS = 0
FETCH NEXT FROM @OutputCursor
DEALLOCATE @OutputCursor
CLOSE COrdersComplete
DEALLOCATE CCategories
DEALLOCATE CCustomers
DEALLOCATE COrdersComplete
Note
Books Online contains a full description of the sp_cursor_list, sp_describe_cursor,
sp_describe_cursor_columns, and sp_describe_cursor_tables system stored
procedures.
Use this information to interpret the output from Listing 12.22.
Microsoft SQL Server 2000 Programming by Example
498
Using Cursors to Solve Multirow Actions in Triggers
In many cases, dealing with multirow operations inside triggers is not an easy task. If the single-row solution is
solved, you can use cursors to convert multirow operations into single-row operations inside the trigger, to
apply to them the same proved logic of the single-row cases.
Consider the following example: You want to assign a credit limit to every customer following an automated
process applied by the AssignCreditLimit stored procedure. To automate the process, you can create a
trigger AFTER INSERT to calculate the credit limit for every new customer.
The AssignCreditLimit stored procedure can work with only one customer at a time. However, an
INSERT operation can insert multiple rows at the same time, using INSERT SELECT.
You can create the trigger with two parts; one will deal with single row and the other with multiple rows, and
you will check which part to apply using the result of the @@ROWCOUNT function as described in Listing 12.23.
Listing 12.23 Using Cursors to Convert Multirow Operations into Single-Row Operations Inside
Triggers
USE Northwind
GO
ALTER TABLE Customers
ADD CreditLimit money
GO
CREATE PROCEDURE AssignCreditLimit
@ID nvarchar(5)
AS
Write here your own CreditLimit function
UPDATE Customers
SET CreditLimit = 1000
WHERE CustomerID = @ID
GO
CREATE TRIGGER isr_Customers
ON Customers
FOR INSERT AS
SET NOCOUNT ON
DECLARE @ID nvarchar(5)
IF @@ROWCOUNT > 1
Multirow operation
BEGIN
Open a cursor on the Inserted table
DECLARE NewCustomers CURSOR
FOR SELECT CustomerID
FROM Inserted
Chapter 12. Row-Oriented Processing: Using Cursors
499
ORDER BY CustomerID
OPEN NewCustomers
FETCH NEXT FROM NewCustomers
INTO @ID
WHILE @@FETCH_STATUS = 0
BEGIN
Assign new Credit Limit to every new customer
EXEC AssignCreditLimit @ID
FETCH NEXT FROM NewCustomers
INTO @ID
END
close the cursor
CLOSE NewCustomers
DEALLOCATE NewCustomers
END
ELSE
Single row operation
BEGIN
SELECT @ID = CustomerID
FROM Inserted
IF @ID IS NOT NULL
Assign new Credit Limit to the new customer
EXEC AssignCreditLimit @ID
END
GO
Test it
INSERT customers (CustomerID, CompanyName)
VALUES ('ZZZZZ', 'New Company')
SELECT CreditLimit
FROM Customers
WHERE CustomerID = 'ZZZZZ'
Application Cursors
When a client application requests information from SQL Server using the default settings in ADO, OLE DB,
ODBC, or DB-Library, SQL Server must follow this process:
1. The client application sends a request to SQL Server in a network package. This request can be any
Transact-SQL statement or a batch containing multiple statements.
2. SQL Server interprets the request and creates a query plan to solve the request. The query plan is
compiled and executed.
3. SQL Server packages the results in the minimum number of network packets and sends them to the
user.
Microsoft SQL Server 2000 Programming by Example
500
4. The clients start receiving network packets, and these packets are waiting in the network buffer for the
application to request them.
5. The client application receives the information contained in the network packages row by row.
The client application cannot send any other statement through this connection until the complete result set is
retrieved or cancelled.
This is the most efficient way to retrieve information from SQL Server, and it is called a default result set. It is
equivalent to a FORWARD_ONLY READ_ONLY cursor with a row set size set to one row.
Note
Some articles and books refer to the default result set as a "Firehose" cursor, which is considered
an obsolete term.
SQL Server supports three types of cursors:
• Transact-SQL cursors— These are the cursors you studied in the previous sections of this chapter.
• Application Programming Interface (API) server cursors— These are cursors created in SQL Server,
following requests from the database library, such as ADO, OLE DB, ODBC, or DB-Library. Listings
12.1 and 12.3 contain examples of this type of cursor.
• Client cursors— These cursors are implemented in the client side by the database library. The client
cache contains the complete set of rows returned by the cursor, and it is unnecessary to have any
communication to the server to navigate the cursor.
Caution
Do not mix API cursors with Transact-SQL cursors from a client application, or SQL Server will try
to map an API cursor over Transact-SQL cursors, with unexpected results.
Tip
Use Transact-SQL cursors in stored procedures and triggers and as local cursors in Transact-SQL
batches, to implement cursors that do not require user interaction.
Use API cursors from client applications where the cursor navigation requires user interaction.
Using a default result set is more efficient than using a server cursor, as commented in previous sections in
this chapter.
Caution
Chapter 12. Row-Oriented Processing: Using Cursors
501
You cannot open a server cursor in a stored procedure or batch if it contains anything other than a
single SELECT statement with some specific Transact-SQL statements. In these cases, use a client
cursor instead.
Using server cursors is more efficient than using client cursors because client cursors must cache the
complete result set in the client side, whereas server cursors send to the client the fetched rows only. To open
a client cursor using ADO, you can set the CursorLocation property to adUseClient in the Connection
or Recordset objects. The default value is adUseServer for server API cursor.
What's Next?
In this chapter, you learned how to use Transact-SQL cursors.
In Chapter 13, you will learn about transactions and locks, which are both important aspects of using cursors.
The concurrency of a database application depends directly on how the application manages transactions and
locks.
Chapter 13. Maintaining Data Consistency: Transactions and Locks
503
Chapter 13. Maintaining Data Consistency: Transactions
and Locks
SQL Server 2000 is designed to serve multiuser environments. If multiple users try to access the same data,
SQL Server must protect the data to avoid conflicting requests from different processes. SQL Server uses
transactions and locks to prevent concurrency problems, such as avoiding simultaneous modifications to the
same data from different users.
This chapter teaches you the following:
• Basic concepts about transactions
• How to use Transact-SQL statements to manage transactions
• How to understand the common concurrency problems and avoid them when they arise
• How to apply the right transaction isolation level
• Lock types available in SQL Server
• How to detect and avoid deadlocks
Characteristics of Transactions (ACID)
A transaction is a sequence of operations executed as a single logical operation, which must expose the ACID
(Atomicity, Consistency, Isolation, and Durability) properties. These are as follows:
• Atomicity— The transaction must be executed as an atomic unit of work, which means that it either
completes all of its data modifications or none at all.
• Consistency— The data is consistent before the transaction begins, and the data is consistent after
the transaction finishes. To maintain consistency, all integrity checks, constraints, rules, and triggers
must be applied to the data during the transaction. A transaction can affect some internal SQL Server
data structures, such as allocation maps and indexes, and SQL Server must guarantee that these
internal modifications are applied consistently. If the transaction is cancelled, the data should go back
to the same consistent state it was in at the beginning of the transaction.
• Isolation— The transaction must be isolated from changes made to the data by other transactions, to
prevent using provisional data that is not committed. This implies that the transaction must either see
the data in its previous state or the transaction must wait until the changes from other transactions are
committed.
• Durability— After the transaction completes, its changes to the data are permanent, regardless of the
event of a system failure. In other words, when a client application receives notification that a
transaction has completed its work successfully, it is guaranteed that the data is changed permanently.
Every RDBMS uses different ways to enforce these properties. SQL Server 2000 uses Transact-SQL
statements to control the boundaries of transactions to guarantee which operations must be considered as an
atomic unit of work.
Constraints and other integrity mechanisms are used to enforce logical consistency of every transaction. SQL
Server internal engines are designed to provide physical internal consistency to every operation that modifies
data, maintaining allocation structures, indexes, and metadata.
The programmer must enforce correct transaction and error management to enforce an appropriate atomicity
and consistency. Later in this chapter, in the "Transactions and Runtime Errors" section, you will learn
about transaction and error management.
Programmers can select the right level of isolation by specifying Transaction Isolation Level or using locking
hints. Later in this chapter, in the "Isolation Levels" section, you will learn how to apply transaction isolation
levels. The section "Types of Locks" gives you details on how to use locking hints.
SQL Server guarantees durability by using the Transaction log to track all the changes to the database and
uses the recovery process when necessary to enforce data consistency in case of system failure or
unexpected shutdown.
Using Transactions
Microsoft SQL Server 2000 Programming by Example
504
To consider several operations as members of the same transaction, it is necessary to establish the
transaction boundaries by selecting the transaction starting and ending points.
You can consider three different types of transactions:
• Auto commit transactions— SQL Server always starts a transaction whenever any statement needs to
modify data. SQL Server automatically commits the transaction if the statement finishes its work
successfully. However, if the statement produces any error, SQL Server will automatically roll back all
changes produced by this incomplete statement. In this way, SQL Server automatically maintains data
consistency for every statement that modifies data.
• Explicit transactions— The programmer specifically declares the transaction starting point and decides
either to commit or rollback changes depending on programming conditions.
• Implicit transactions— SQL Server starts a transaction automatically whenever any statement needs to
modify data, but it is the programmer's responsibility to specify the transaction ending point and
confirm or reject applied changes.
Note
It is impossible to instruct SQL Server to disable the creation of Auto commit transactions. This is
why inside a trigger you are always inside a transaction.
A Transact-SQL batch is not a transaction unless stated specifically. In Listing 13.1, Operations 1 through 3
are independent; there is no link between them, so they don't form a single transaction. If there is an error in
one of the operations, the others can still be committed automatically. However, operations 4 through 6 are
part of the same transaction, and either all of them or none of them will be applied permanently.
Using the @@IDENTITY function can be wrong in this case, because this system function returns the latest
Identity value generated in this connection. If a trigger inserts data in a table where you have an Identity field,
the @@IDENTITY function will return the value generated inside the trigger, not the one generated by the
original action that fired the trigger.
Tip
Use the SCOPE_IDENTITY() function to retrieve the latest Identity value inserted in the current
scope.
Listing 13.1 Setting the Transaction Boundaries
USE Northwind
Chapter 13. Maintaining Data Consistency: Transactions and Locks
505
GO
Without Transactions
DECLARE @CatID int,
@ProdID int
Operation 1
Create a new Category
INSERT Categories
(CategoryName)
VALUES ('Cars')
Retrieves the latest IDENTITY value inserted
SET @CatID = SCOPE_IDENTITY()
Operation 2
Create a new product
in the new Category
INSERT Products
(ProductName, CategoryID)
VALUES ('BigCars', @CatID)
Retrieves the latest IDENTITY value inserted
SET @ProdID = SCOPE_IDENTITY()
Operation 3
Change UnitsInStock
for the new product
UPDATE Products
SET UnitsInStock = 20
WHERE ProductID = @ProdID
With Transactions
Start a new transaction
BEGIN TRAN
Operation 4
Create a new Category
INSERT Categories
(CategoryName)
VALUES ('HiFi')
IF @@ERROR <> 0 GOTO AbortTransaction
SELECT @CatID = CategoryID
FROM Categories
Microsoft SQL Server 2000 Programming by Example
506
WHERE CategoryName = 'HiFi'
Operation 2
Create a new product
in the new Category
INSERT Products
(ProductName, CategoryID)
VALUES ('GreatSound', @CatID)
IF @@ERROR <> 0 GOTO AbortTransaction
SELECT @ProdID = ProductID
FROM Products
WHERE ProductName = 'GreatSound'
Operation 3
Change UnitsInStock
for the new product
UPDATE Products
SET UnitsInStock = 50
WHERE ProductID = @ProdID
IF @@ERROR <> 0 GOTO AbortTransaction
COMMIT TRAN
PRINT 'Transaction committed'
GOTO EndTransaction
AbortTransaction:
ROLLBACK TRAN
PRINT 'Transaction rolled back'
EndTransaction:
PRINT 'Transaction finished'
BEGIN TRAN
To start a new local transaction, you can use the BEGIN TRANSACTION (or BEGIN TRAN) statement. This
statement starts a new transaction, if there aren't any transactions already started, or creates a new level of
nested transactions if the execution was already inside another transaction.
As mentioned before, any time you execute a statement that modifies data, SQL Server automatically starts a
new transaction. If you were already inside a transaction when the statement started to run and this operation
fired a trigger inside the trigger, you will be in the second level of a nested transaction.
The same situation happens if you define a stored procedure to apply some data changes, and you need to
apply these data changes as a single transaction. In this case, you start a new transaction inside the stored
procedure and decide at the end of it whether you want to commit or roll back. This stored procedure will
execute its statements in a transaction state regardless of the existence of a transaction in the calling
procedure or batch.
It is possible to have any number of nested transactions in SQL Server 2000. The @@TRANCOUNT system
function gives you the number of open transactions you have at any given time. Any time you execute BEGIN
TRAN, the result of the function @@TRANCOUNT is increased by one. Listing 13.2 shows an example of how
the @@TRANCOUNT function works.
Listing 13.2 Values of the @@TRANCOUNT Function After Using BEGIN TRAN