INF: Overview of SQL Server, ODBC, and DB-Library Cursors

ID: Q156489


The information in this article applies to:


SUMMARY

This article outlines the cursor models offered by SQL Server 6.0 server cursors, the DB-Library Cursor Library, and Open Database Connectivity (ODBC). Users who want a more in-depth discussion of cursors can read the section on the ODBC Cursor Model in "Inside ODBC" by Kyle Geiger, available from Microsoft Press in most bookstores.


MORE INFORMATION

All operations in a relational database operate on a complete set of rows. For example, an UPDATE command works on the set of all rows that satisfy the conditions in the UPDATE's where clause. However, there are times when an application's work can be more easily implemented by performing a series of operations based on subsets of rows in a result set, or by working through a result set one row at a time. This is especially true for interactive client-server applications that present result set rows to a user who wants to scroll up and down the result set in blocks of rows at a time.

Cursors offer applications the ability to process through a result set one row, or one block of rows, at a time. Think of a cursor as a pointer to a current position, or a specific row, within the result set of a single SELECT command. The following is a simple example of an application using a cursor to retrieve all the rows that result from a SELECT:


   DECLARE cursor_name CURSOR FOR select * from some_table
   OPEN cursor_name
   While there are rows to fetch
      FETCH NEXT FROM cursor_name
   CLOSE cursor_name
   DEALLOCATE cursor_name 

The real power of cursors is that the FETCH command can be used to scroll around within the result set, not just going through one row at a time. The following table shows what the application can do with different FETCH commands:

Command                  Result
------------------------------------------------------------------------------
FETCH FIRST              Positions the cursor on the first row

FETCH RELATIVE X         Jumps the cursor  X rows from the current position

FETCH ABSOLUTE Y         Positions the cursor Y rows from the start of a
                         result set

FETCH PRIOR              Reads the row just before the current row 

Once positioned on a row in a cursor, an application can work with the data from that row.

Some cursors also support UPDATE operations, and applications can perform an UPDATE on the row where the cursor is currently positioned. In the case of a cursor built using a SELECT that references multiple tables, the UPDATE command can only reference one of the tables. Therefore, the operation affects only the table actually named in the UPDATE command. For example, in the following operation, only one row in some_table_b would be deleted; no rows in some_table_a would be affected:

   DECLARE cursor_name CURSOR FOR
      select *
      from some_table_a A, some_table_b B
      where A.PrimaryKey = B.ForeignKey
   OPEN cursor_name
   FETCH FIRST FROM cursor_name
   DELETE FROM some_table_b WHERE CURRENT OF cursor_name
   CLOSE cursor_name
   DEALLOCATE cursor_name 

ODBC and DB-Library cursors are used by applications written in languages such as C, C++, or Visual Basic using the ODBC or DB-Library APIs. The server cursors introduced with SQL Server 6.0 are used in Transact-SQL scripts and stored procedures. They are also used to implement the cursors in ODBC and DB-Library applications. When either the SQL Server ODBC driver or a DB-Library application is asked to open a cursor, they implement the cursor by opening a server cursor of the same type. The ODBC or DB-Library application is not aware that underneath the API, it is actually working with server cursors.

The cursors defined in ANSI SQL only support operations that act upon one row at a time. The cursors defined in DB-Library and ODBC extend this functionality to support multiple row operations in cursors. ODBC does this by introducing the concept of a rowset size. If an ODBC application sets the rowset size option to 10, then each FETCH operation will retrieve 10 rows. DB-Library offers the same functionality through the nrows parameter of dbcursoropen().

The remainder of this article describes the operation of DB-Library and ODBC cursors; doing so requires defining all of the cursor options. After describing these cursors and defining the principles, it will then be easier to describe the simplified ANSI-style cursors available in Transact- SQL.

Originally, Microsoft DB-Library did not use the concept of cursors. An application ran a SELECT, and then read the rows back sequentially, one row at a time in a forward direction, until the end of the result set was reached. The result set did not support positioned updates; it was simply the stream of rows that came back from the SELECT. If an application wanted to UPDATE, INSERT, or DELETE one of the rows in the result set, it had to build an UPDATE command targeted at the base table referenced by the SELECT, and then reference the relevant key values in the where clause of the UPDATE command.

The version of DB-Library that came with SQL Server 4.2 for OS/2 introduced the DB-Library Cursor Library, which supported scrollable, updatable cursors. Starting with SQL Server 6.0, the DB-Library Cursor Library implemented its cursors by opening the server cursors introduced in SQL Server 6.0. A DB-Library application would still either open a standard result set, or use the Cursor Library if it wanted a scrollable, updatable cursor.

DB-Library classifies its cursors into three categories. A client cursor is one implemented on the client; these are the only cursors supported in SQL Server 4.21a and earlier. These cursors can also be implemented in SQL Server 6.0 and later, if the application sets the DBCLIENTCURSORS option. An explicit server cursor is implemented under SQL Server 6.0 and later, if the first FETCH is done using the extended function dbcursorfetchex(). A transparent server cursor is implemented under SQL Server 6.0 and later if the first FETCH is done using the standard dbcursorfetch(). There are some differences between transparent and explicit cursors when fetching rows. Refer to the "Programming DB-Library 6.0 for C" manual for the specific distinctions.

The ODBC API does not make a distinction between a result set and a cursor. In the ODBC model, all SQL commands return a result set, which an application fetches through using SQLFetch() or SQLExtendedFetch(). Before running a SQL command, an ODBC application can set statement options to control the cursor behavior of the result set. These options and their default settings are:

   Option                Default
   --------------------------------------

   SQL_CURSOR_TYPE       SQL_FORWARD_ONLY

   SQL_CONCURRENCY       SQL_READ_ONLY

   SQL_ROWSET_SIZE       1 

These settings are identical to the characteristics of the default result set in DB-Library. If an ODBC application wants cursor behavior, it simply changes these options to the desired setting before running a SQL command. If an ODBC application runs a command with the default settings, the Microsoft SQL Server ODBC driver will process the result set the same way a DB-Library application would process its standard result set. Prior to SQL Server 6.0, this was the only option supported by the Microsoft SQL Server ODBC driver, unless the application used the ODBC Cursor Library (see Appendix G of the ODBC 2.0 Programmer's Reference for more information on the ODBC Cursor Library). Starting with SQL Server 6.0, the Microsoft SQL Server ODBC driver supports the other cursor options by using the server cursors introduced with SQL Server 6.0. If any of these statement options are changed from their defaults, the Microsoft SQL Server ODBC driver implements the cursor functionality by opening a server cursor with the same characteristics.

ODBC applications also do not typically perform positioned updates using the WHERE CURRENT OF cursor_name syntax on UPDATE commands. Instead, ODBC applications typically use the ODBC API function SQLSetPos() for positioned updates. For more information, refer to the ODBC 2.0 Programmer's Guide.

Cursor Types

ODBC and DB-Library share the following five cursor types:

Concurrency Options

In addition to the cursor types, cursor operations are also affected by the concurrency options set by the application:

ODBC: SQL_CONCURRENCY = SQL_CONCUR_READONLY
DB-Library: concuropt = CUR_READONLY

With this option set, the cursor will not support UPDATE commands. Locks are not held on the underlying rows that make up the result set.

ODBC: SQL_CONCURRENCY = SQL_CONCUR_VALUES
DB-Library: concuropt = CUR_OPTCCVAL

This is the first option offering optimistic concurrency control. Optimistic concurrency control is a standard part of transaction control theory, and is discussed in most papers and books on the subject. The user or application chooses optimistic control when "optimistic" that there is a slight chance that anyone else may have updated a row in the interval between when the cursor is opened and when the row is updated. When the cursor is opened in this mode, no locks are held on the underlying rows, in order to maximize throughput. If the user attempts an UPDATE, SQL Server compares the current values in the row with the values retrieved when the cursor was opened. If any of the values have changed, the engine knows that someone else has already updated the row, and it returns an error. If the values are the same, the cursor engine performs the UPDATE. Selecting this option causes the user or application to accept the responsibility of dealing with an occasional error indicating that another user updated the row and changed the values. A typical action taken by an application that receives this error would be to refresh the cursor, to get the new values, and then let the user decide if he or she still wants to perform the UPDATE on the new values.

ODBC: SQL_CONCURRENCY = SQL_CONCUR_ROWVER
DB-Library: concuropt = CUR_OPTCC

This is the second optimistic concurrency control option, based on row versioning. With row versioning, the underlying table must have a version identifier of some type that the cursor engine can use to determine whether the row has been changed since it was read into the cursor. In SQL Server, this is the facility offered by the timestamp datatype. SQL Server timestamps are binary numbers that indicate the relative sequence of modifications in a database. Each database has a global current timestamp value. Each time a page in the database is modified in any way, SQL Server stores the current value in the page, and then increments it. If a table has a timestamp column, then the timestamps are taken down to the row level. Each time any row is modified, its timestamp column is updated with the current timestamp.



The cursor engine can then compare a row's current timestamp value with the timestamp value that was first retrieved into the cursor, to determine whether the row has been updated. The engine does not have to compare the values in all columns, only the timestamp. If an application requests SQL_CONCUR_ROWVER (ODBC) or CUR_OPTCC (DB-Library) on a table that does not have a timestamp column, the cursor will default to the values-based optimistic concurrency control, SQL_CONCUR_VALUES or CUR_OPTCCVAL.

ODBC: SQL_CONCURRENCY = SQL_CONCUR_LOCK
DB-Library: concuropt = CUR_LOCKCC

This option implements pessimistic concurrency control, in which the application attempts to lock the underlying database rows at the time they are read into the cursor result set. For cursors using server cursors, an update intent lock is placed on the row when it is read into the cursor. If the cursor is opened within a transaction, these intent-to-update locks are held until the transaction is terminated (committed or rolled back). If the cursor has been opened outside a transaction, the lock is dropped immediately after the row has been read into the cursor. Thus, applications wanting full pessimistic concurrency control would typically open the cursor within a transaction. An update intent lock prevents any other task from acquiring an update intent or exclusive lock; this prevents any other task from updating the row. An update intent lock, however, will not block a shared lock, so it does not prevent other tasks from reading the row unless the second task is also requesting a read with an update intent lock.

Please note that in all of these concurrency options, when any row in the cursor is updated, SQL Server will lock it with an exclusive lock. If the update has been done within a transaction, the exclusive lock will be held until the transaction is terminated. If the update has been done outside of a transaction, the update is automatically committed when it is completed and the exclusive lock is freed. Because SQL Server must acquire an exclusive lock before it updates the row, updates done through a cursor (just like standard updates) can be blocked by other connections holding a share lock on the row.

Transaction Isolation Levels

The full locking behavior of cursors is based on an interaction between the concurrency options discussed above and the transaction isolation level set by the client. ODBC clients set the transaction isolation level by setting the connection option SQL_TXN_ISOLATION. DB-Library applications and Transact-SQL scripts set the isolation level by running the Transact-SQL SET TRANSACTION ISOLATION LEVEL command. Users should combine the read locking behaviors of the combination of the concurrency and transaction isolation level options to determine the full locking behavior of a specific cursor environment.

READ COMMITTED (The default for both SQL Server and ODBC)
ODBC: SQL_TXN_ISOLATION = SQL_TXN_READ_COMMITED
DB-Library: SET TRANSACTION ISOLATION LEVEL READ COMMITTED

SQL Server acquires a shared lock while reading a row into a cursor, but frees the lock immediately after reading the row. Because a shared lock request will be blocked by an exclusive lock, a cursor is prevented from reading a row that another task has updated but not yet committed.

READ UNCOMMITTED
ODBC: SQL_TXN_ISOLATION = SQL_TXN_READ_UNCOMMITED
DB-Library: SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED

SQL Server requests no locks while reading a row into a cursor, and honors no exclusive locks. This means that cursors can be populated with values that have already been updated, but not yet committed. The user is bypassing all of SQL Server's locking transaction control mechanisms.

REPEATABLE READ or SERIALIZABLE
ODBC: SQL_TXN_ISOLATION = SQL_TXN_REPEATABLE_READ_UNCOMMITED
   or SQL_TXN_ISOLATION = SQL_TXN_SERIALIZABLE
DB-Library: SET TRANSACTION ISOLATION LEVEL REPEATABLE READ
         or SET TRANSACTION ISOLATION LEVEL SERIALIZABLE 

SQL Server still requests a shared lock on each row as it is read into the cursor as in READ COMMITTED, but if the cursor is opened within a transaction, the shared locks are held until the end of the transaction, instead of being freed after the row is read. This is the same effect as specifying HOLDLOCK on a select command.

Note that the ODBC API specifies additional transaction isolation levels, but these are not supported by SQL Server or the SQL Server ODBC Driver.

Transact-SQL Cursor Types

The ODBC and DB-Library cursor types above offer extensions beyond the ANSI cursor model, primarily to support the functions needed in interactive applications. The server cursors in Transact-SQL are implemented to the ANSI cursor standard, so they do not offer all the options of ODBC and DB-Library cursors. Because Transact-SQL cursors are used in stored procedures and triggers, they do not need the interactive extensions in DB-Library and ODBC cursors.

The following characteristics of Transact-SQL cursors are set when the cursor is declared:

DECLARE cursor_name [INSENSITIVE] [SCROLL] CURSOR
FOR select_statement
[FOR {READ ONLY | UPDATE [OF column_list]}]

The type of cursor implemented depends on the combination of SCROLL and INSENSITIVE options specified.

DECLARE cursor_name INSENSITIVE CURSOR ...

The resulting cursor will be an insensitive, or static, cursor. It will also be forward_only and read_only.

DECLARE cursor_name INSENSITIVE SCROLL CURSOR ...

The resulting cursor will be similar to the INSENSITIVE cursor; the only difference is that it will be scrollable instead of forward_only.

DECLARE cursor_name SCROLL CURSOR ...

The resulting cursor will be implemented as a keyset cursor. If there is no unique index on the underlying table the cursor will be read_only.

DECLARE cursor_name CURSOR ...

The resulting cursor will be implemented as a forward_only cursor.

In SQL Server 6.0, forward_only cursors default to keyset cursors. The cursor will be created as INSENSITIVE (read_only) if any of the following conditions are true:
In SQL Server 6.5, forward_only cursors default to dynamic cursors. If the cursor's select clause has an ORDER BY, the cursor will be created as INSENSITIVE, unless the underlying table has a unique index.

Additional query words: dblib db-lib tsql t-sql tran-sql


Keywords          : kbusage SSrvDB_Lib SSrvGen SSrvTran_SQL 
Version           : 6.0
Platform          : WINDOWS 
Issue type        : kbhowto 

Last Reviewed: April 2, 1999