Tải bản đầy đủ (.pdf) (71 trang)

Microsoft SQL Server 2000 Programming by Example phần 8 pdf

Bạn đang xem bản rút gọn của tài liệu. Xem và tải ngay bản đầy đủ của tài liệu tại đây (646.7 KB, 71 trang )

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

×