2 of my favorite new T-SQL features in SQL Server Denali (SQL 11)

Microsoft released the first CTP version of SQL Server Denali during the SQLPASS Community Summit in Seattle last year. Since this major event in the SQL Server community I had a little bit of time to look into the CTP 1 version of Denali and see what new features they are providing us. 2 interesting features in the area of T-SQL I want to cover today throughout this blog post: Sequences and Paging

Let’s start with sequences first. If you already worked with Oracle you should be already familiar with sequences, because they are used everywhere in the database design. For SQL Server sequences are a completely know concept. A sequence is nothing else like an INT IDENTITY column, but with the difference that a sequence is created on the database level and can be used and accessed through all your database objects (like tables, stored procedures, stored functions, etc.). Sequences are created with the new T-SQL statement CREATE SEQUENCE:

CREATE SEQUENCE MyNewSequence
    AS INT
    MINVALUE 1
    MAXVALUE 1000
    START WITH 1
GO

As you can see, the sequence get’s a name which must be unique within the current database. Then you have to specify the underlying data type of the sequence, like TINYINT, SMALLINT, INT, BIGINT. You can also specify a minimum and maximum value (MINVALUE, MAXVALUE) and which value your sequence should return, when the sequence is accessed for the first time (START WITH). To track all your sequences within a database, SQL Server provides you a new system view – sys.sequences:

SELECT * FROM sys.sequences
GO

After you have created a new sequence you can start using it through the new command NEXT VALUE FOR:

DECLARE @id INT
SET @id = NEXT VALUE FOR MyNewSequence
SELECT @id
GO

In this case SQL Server retrieves the next value from the specified sequence. In this case you get back the value 1. If you execute the statement several times, SQL Server returns sequentially the value 2, 3, 4, 5, 6, etc. It’s the same like a INT IDENTITY column, but defined and used on the database level. Existing sequences can be also altered through the ALTER SEQUENCE statement:

ALTER SEQUENCE MyNewSequence
    RESTART WITH 10
    INCREMENT BY 10
    NO MAXVALUE
GO

As you can see, you can restart the sequence on a specified value, and you have also the possibility to specify an increment value and that you don’t want to use a maximum value. If you specify the NO MAXVALUE option, the maximum value is defined through the underlying data type, that is used by that sequence. Therefore it is also not allowed to define the following sequence:

CREATE SEQUENCE InvalidSequence
    AS TINYINT
    MINVALUE 260
    MAXVALUE 300
    START WITH 260
GO

The data type TINYINT has a maximum value of 255 in SQL Server, therefore you can define with this underlying data type a sequence for the range of 260 to 300. SQL Server will return you an error message. You are also not allowed to define sequences that are counting down, like:

CREATE SEQUENCE InvalidSequence
    AS TINYINT
    MINVALUE 200
    MAXVALUE 1
    START WITH 200
    INCREMENT BY -1
GO

Another nice feature of sequences is cycling, which means that the sequence returns the first values in the sequence when it has reached the specified maximum value. See the following example for an explanation:

CREATE SEQUENCE CyclingSequence
    AS INT
    MINVALUE 1
    MAXVALUE 10
    CYCLE
GO

DECLARE @id INT
SET @id = NEXT VALUE FOR CyclingSequence
SELECT @id
GO 15

The first statement creates a cycling sequence with a range from 1 to 10. If you execute the next batch 15 times (GO 15), SQL Server returns 15 values from the sequence: 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5 – the sequence is just cycling through its range.

If you are working with sequences inside transactions you must be also aware that sequences are not providing you a consistent gap-free stream of values. This is important when your transaction is rolled back:

BEGIN TRANSACTION
DECLARE @id INT
SET @id = NEXT VALUE FOR CyclingSequence
SELECT @id
GO

ROLLBACK TRANSACTION
DECLARE @id INT
SET @id = NEXT VALUE FOR CyclingSequence
SELECT @id
GO

If you have retrieved a value from a sequence and you finally rollback your transaction, then the value is consumed, and NEXT VALUE FOR returns the next value from your sequence. Short story: sequences are not aware of transactions! If you want to retrieve a whole range of sequence values, you don’t have to use a FOR loop for it. SQL Server provides you in this case the handy system stored procedure sys.sp_sequence_get_range:

DECLARE @firstValue SQL_VARIANT, @lastValue SQL_VARIANT;
EXEC sys.sp_sequence_get_range
    @sequence_name     = ‘CyclingSequence’,
    @range_size        = 3,
    @range_first_value = @firstValue OUTPUT,
    @range_last_value  = @lastValue OUTPUT;
SELECT 
    FirstValue = CONVERT(INT, @firstValue), 
    LastVlaue = CONVERT(INT, @lastValue),
    NextValue = NEXT VALUE FOR CyclingSequence
GO

The important point to mention here is the fact, that sys.sp_sequence_get_range just marks a range with the starting- and ending value as used. That means that you can use the sequence values within this range without ever retrieving them explicitly. If you mark for example the range from 5 to 10 as used, then you can use the values 5, 6, 7, 8, 9, and 10 completely at your own inside your logic. You don’t have to retrieve them again through NEXT VALUE FOR, because the whole range was already allocated through sys.sp_sequence_get_range. When you don’t need your sequence anymore you can delete it from your database:

DROP SEQUENCE MyNewSequence
GO

As you can see from this explanation sequences are a very handy concept when you want to work with unique values across your whole database. In the past I have seen a lot of database designs where unique values where needed across different tables. That was not possible with INT IDENTITY values, because they are scoped to a specified table. Some people used here a centralized table which stored the current value that was used across the database. That approach is not needed any more with sequences. From an application development perspective sequences are also very cool, because you can retrieve a unique value from SQL Server before ever committing your record to the table (like with INT IDENTITY values, which are calculated only when a new record is inserted into a table). Therefore you have now really no reason anymore to use GUIDs as PKs (and therefore by default as clustered keys) in your database design, which causes you A LOT of overhead and index fragmentation throughout your whole database. Kimberly Tripp has written in the last month a few post blogs about this specified problem, and I recommend to to read it, and think very careful about it when you have defined PKs on UNIQUEIDENTIFIER data types…

Another new nice feature in SQL Server Denali is NATIVE paging support, which means you don’t have to use a workaround which is possible with the ROW_NUMBER() function introduced with SQL Server 2005:

DECLARE
	@PageSize    TINYINT = 10,
	@CurrentPage INT     = 100;
WITH o AS
(
    SELECT TOP (@CurrentPage * @PageSize)
        [RowNumber] = ROW_NUMBER() OVER (ORDER BY SalesOrderDetailID),
        SalesOrderDetailID,
        OrderQty,
        UnitPrice,
        LineTotal
    FROM
        Sales.SalesOrderDetail
)
SELECT
    SalesOrderDetailID,
    OrderQty,
    UnitPrice,
    LineTotal
FROM o
WHERE
    [RowNumber] BETWEEN ((@CurrentPage – 1) * @PageSize + 1)
    AND (((@CurrentPage – 1) * @PageSize) + @PageSize)
ORDER BY
    [RowNumber]
GO

When you want to use paging, you specify the record position that you want to retrieve first from your result set. If your result set holds 1000 records, and you specify the OFFSET of 500, SQL Server just skips the first 499 records, and starts returning back your records at the 500th record. Finally you can specify with FETCH NEXT the amount of records that you want to get back starting at the specified OFFSET:

DECLARE
    @PageSize    BIGINT = 10,
    @CurrentPage INT     = 100;
SELECT
    SalesOrderDetailID,
    OrderQty,
    UnitPrice,
    LineTotal
FROM Sales.SalesOrderDetail
ORDER BY SalesOrderDetailID -- The ORDER BY clause is needed by the paging mechanism!!!
OFFSET (@PageSize * (@CurrentPage – 1)) ROWS -- Specifies the number of rows to skip before it starts to return rows from the query expression
FETCH NEXT @PageSize ROWS ONLY -- Specifies the number of rows to return after the OFFSET clause has been processed
GO

Please be aware that the paging functionality always needs a sorted result set, therefore you have to sort it through ORDER BY, otherwise SQL Server returns you an error message.

-Klaus