Developers and database administrators have long debated methods for paging recordset results from Microsoft SQL Server, trying to balance ease of use with performance. The simplest methods were less efficient because they retrieved entire datasets from SQL Server before eliminating records which were not to be included, while the best-performing methods handled all paging on the server with more comple scripting. The !"#$%&M'(!)* !"#$%&M'(!)* function introduced in SQL Server + provides an efficient way to limit results relatively easily. easily. Paging Efficiency
n order to scale well, most applications only wor/ with a portion of the available data at a given time. #eb-based data maintenance applications are the most common eample of this, and several databindable 0S1.%(T classes )such as 2rid3iew and Datagrid* have built-in support for paging results. #hile it is possible to handle paging within the web page code, this may re4uire transferring all of the dataisfrom the database server to the weband server every ,time control updated. To improve performance efficiency efficiency, datathe which will not be used should be eliminated from processing as early as possible. Paging Methods
Many popular databases offer functions allowing you to limit which rows are returned for a given 4uery based upon their position within the record set. 5or eample, MySQL MySQL provides the LMT 4ualifier, which ta/es two parameters. The first LMT parameter specifies which )6ero-based* row number will be the first record returned, and the second parameter specifies the maimum number of records returned. The 4uery7 SELECT * FROM table LIMIT 20,13
...will return the +th through the 8+nd records -- assuming at least 88 records are available to return. f fewer than 88 records are available, the 4uery will return all records from record + on. f fewer than + records are available, none will be returned. SQL Server does not have this functionality functionality,, however the + release does have a number of other new tric/s. 5or instance, support for 9L! procedures means it is possible to use eisting paging methods to write 3'.%(T or 9: code that would eecute within the SQL Server environment. &nfortunately, 9L! procedures
are not as efficient as native Transact SQL. To ensure best performance, 4ueries should still be written in TSQL whenever practical. Using ROW_NUMBER()
TSQL in the + release includes the !"#$%&M'( !"#$%&M'(!)* !)* function, which adds an integer field to each record with the record;s ordinal result set number. Stated more simply, it adds the record;s position within the result set as an additional field so that the first record has a <, the second a +, etc. This may appear to be of little value, however by using nested 4ueries we can use this to our advantage. To demonstrate !"#$%&M'(!)* !"#$%&M'(!)* and to eplore how the paging solution wor/s, create a simple salary table and populate it with random data using the following commands7 CREATE TABLE [dbo].[Salarie]! ["ero#] [#$ar%&ar]!'0( [#$ar%&ar]!'0 ( )OT )LL, [i#%o+e] [+o#e] )OT )LL, CO)STRAI)T -RIMAR E CLSTERE! ["ero#][-/alarie] ASC (( O) [-RIMAR] O I)SERT I)SERT I)SERT I)SERT I)SERT I)SERT I)SERT I)SERT I)SERT I)SERT I)SERT I)SERT I)SERT I)SERT I)SERT I)SERT I)SERT
The !"#$%&M'(!)* !"#$%&M'(!)* function has no parameters - it simply adds the row number to each record in the result set. To ensure the numbering is consistent, however, SQL Server needs to /now how to sort the data. 'ecause of this, !"#$%&M'(!)* must immediately be followed by the "3(!)* function. "3(!)* has one re4uired parameter, which is an "!D(! '= clause. The basic synta for 4uerying the Salaries table is7
SELECT RO</)MBER!( OER!ORER B "ero#(, "ero#, i#%o+e FROM Salarie
This returns the following result7 (No (N o co colu lumn mn na name me)) pe perso rson n
in inco come me
1
Amelia
36000.00
2 3
Anna 49000.00 Constance 51000.00
4
Danielle
5
Elia!et" 23000.00
6
Emerson 84000.00
#
E$a
51000.00
8
%oe
28 28000.00
9
%o"n
6#000.00
10
%or&e
48000.00
11
'aren
#3000.00
12 13
ic"ael alp"
45000.00 18000.00
14
*tanle+
59000.00
15
*tep"anie 4#000.00
16
*ue
9 96 6000.00
1#
,al-o
4#000.00
68000.00
The Salaries data now appears sorted by person, and it has an etra column indicating each record;s position within the results. f for any reason you wanted the results to display in a different order than they were numbered in, you can include a different "!D(! '= clause as part of the normal S(L(9T synta7 SELECT RO</)MBER!( OER!ORER B "ero#(, "ero#, i#%o+e FROM Salarie ORER B i#%o+e
This returns the following result7 (No (N o co colu lumn mn na name me)) pe perso rson n
in inco come me
13
alp"
18000.00
5
Elia!et" 23000.00
8 1
%oe Amelia
28 28000.00 36000.00
12
ic"ael
45000.00
15
*tep"anie 4#000.00
1#
,al-o
4#000.00
10
%or&e
48000.00
2
Anna
49000.00
3
Constance 51000.00
#
E$a
51000.00
14
*tanle+
59000.00
9
%o"n
6#000.00
4
Danielle
68000.00
11
'aren
#3000.00
6
Emerson 84000.00
16
*ue
9 96 6000.00
f we want to limit the results displayed to a certain range, we need to nest this S(L(9T inside another one and provide a name for the !"#$%&M'(!)* column. To limit our results to records through >, !"#$%&M'(!)* we can use the following 4uery7 SELECT * FROM !SELECT RO</)MBER!( OER!ORER B "ero#( AS ro?#7+, "ero#, i#%o+e FROM Salarie( AS Salarie1 <@ERE ro?#7+ ' A) ro?#7+ 8
This returns the following result7 ron ronum um pe perso rson n
in inco come me
5
Elia!et" 23000.00
6
Emerson 84000.00
#
E$a
51000.00
8
%oe
28000.00
9
%o"n
6#000.00
0gain, we can change the sort order by adding an "!D(! "!D(! '= clause. This is most easily accomplished by using the outer S(L(9T statement7 SELECT * FROM !SELECT RO</)MBER!( OER!ORER B "ero#( AS ro?#7+, "ero#, i#%o+e FROM Salarie( AS Salarie1 <@ERE ro?#7+ ' A) ro?#7+ 8 ORER B i#%o+e
This returns the following result7 ron ronum um pe perso rson n
in inco come me
5
Elia!et" 23000.00
8
%oe
28000.00
#
E$a
51000.00
9 6
%o"n 6#000.00 Emerson 84000.00
f we want to support the same type of arguments that MySQL;s LMT)* supports, we can create a stored procedure that accepts a beginning point and a maimum number of records to return. !"#$%&M'(! !"#$%&M '(! re4uires that the data be sorted, so we will also have a re4uired parameter for the "!D(! '= clause. (ecute the following statement to create a new stored procedure7 CREATE -ROCERE [dbo].["a=eSalarie] Dtart i#t 1 ,D+a%t i#t ' ,Dort #$ar%&ar!200 #$ar%&ar!200( ( AS SET )OCO)T O) ECLARE DSTMT #$ar%&ar!+a(, #$ar%&ar!+a(, SGL tate+e#t to ee%7te D7bo7#d i#t IF Dtart Dtart IF D+a%t D+a%t SET D7bo7#d SET DSTMT 4( AS ro?, * EEC !DSTMT(
1 1 4
SET Dtart 1 SET D+a%t 1 Dtart H D+a%t SELECT "ero#, i#%o+e FROM ! SELECT RO</)MBER!( OER!ORER B 4 H Dort H FROM Salarie ( AS tbl <@ERE
ro? 4 H CO)ERT!$ar%&ar!8(, CO)ERT!$ar%&ar!8(, Dtart( H 4 A) ro? 4 H CO)ERT!$ar%&ar!8(, CO)ERT!$ar%&ar!8(, D7bo7#d( ret7r# reJ7eted re%ord
The pageSalaries procedure begins with S(T %"9"&%T "% to disable the record count message )a common step for optimi6ing 4uery performance*. #e then declare two necessary variables, ?STMT and ?ubound. 'ecause we want to be able to change what "!D(! '= argument is used, we need to dynamically generate our 4uery statement by storing it in ?STMT. The net lines ensure that only positive numbers are used for the starting position and maimum si6e, then calculate the range of !"#$%&M !"#$%&M'(!)* '(!)* values being re4uested. )f we wanted to be 6ero-based li/e MySQL;s LMT, we could do so with a few minor twea/s.* "nce the dynamic SQL
command has been strung together, it is eecuted so that the results are returned. (ecute the following statement to test the stored procedure7 "a=eSalarie :, ;, 4i#%o+e4
This returns the following result7 person
income
Amel Am elia ia
3600 36000. 0.00 00
ic" i c"ae aell
45 4500 000. 0.00 00
*tep"anie 4#000.00 ,al,a l-o o
4#0 #000 00.0 .00 0
%or&e
48000.00
Anna
49000.00
Constance 51000.00
f we eecute7 "a=eSalarie 13, ;, 4i#%o+e4
we receive bac/7 person
income
%o %o" "n
6#000.00
Danielle 68000.00 'are 'a ren n
#300 #3000. 0.00 00
Emerson 84000.00 *ue
96000.00
... because the 4uery goes beyond the number of records available. Ta/ing this one step further, we can ma/e a stored procedure that does a more general form of paging. n fact, it can be generali6ed to the point that it can be used to return any collection of fields, in any order, with any filtering clause. To create this wunder/ind marvel, eecute the following command7 CREATE -ROCERE [dbo].[7til-AE] Ddatar% #$ar%&ar!200( ,DorderB #$ar%&ar!200( ,DKieldlit #$ar%&ar!200( 4*4 ,DKilter #$ar%&ar!200( 44 ,D"a=e)7+ i#t 1
,D"a=eSi>e i#t )LL AS SET )OCO)T O) ECLARE DSTMT #$ar%&ar!+a( ,Dre%%t i#t "a=i#= i#terKa%e(
SGL to ee%7te total oK re%ord !Kor ridie?
IF LTRIM!RTRIM!DKilter(( 44 SET DKilter 41 14 IFSET D"a=eSi>e )LL BEI) DSTMT IS4SELECT 4 H DKieldlit H 4FROM 4 H Ddatar% H 4<@ERE 4 H DKilter H 4ORER B 4 H DorderB EEC !DSTMT( ret7r# reJ7eted re%ord E) ELSE BEI) SET DSTMT 4SELECT Dre%%t CO)T!*( FROM 4 H Ddatar% H 4 <@ERE 4 H DKilter EEC "/ee%7teSGL "/ee%7teSG L DSTMT, D"ara+ )4Dre%%t I)T OT-T4, Dre%%t Dre%%t OT-T SELECT Dre%%t AS re%%t ret7r# t&e total oK re%ord
ECLARE Dlbo7#d i#t, D7bo7#d i#t
SET D"a=e)7+ ABS!D"a=e)7+( SET D"a=eSi>e ABS!D"a=eSi>e( ABS!D"a=eSi>e( IF D"a=e)7+ 1 SET D"a=e)7+ 1 IF D"a=eSi>e 1 SET D"a=eSi>e 1 SET Dlbo7#d !!D"a=e)7+ 1( * D"a=eSi>e( SET D7bo7#d Dlbo7#d H D"a=eSi>e H 1 IF Dlbo7#d Dre%%t BEI) SET D7bo7#d Dre%%t H 1 SET Dlbo7#d D7bo7#d !D"a=eSi>e H 1( ret7r# t&e lat "a=e oK re%ord iK #o re%ord ?o7ld be o# t&e "e%iKied "a=e E) SET DSTMT 4SELECT 4 H DKieldlit H 4 FROM ! DorderB H 4( AS ro?, * <@ERE A) EEC !DSTMT( E)
SELECT FROM <@ERE ( AS tbl
RO</)MBER!( OER!ORER B 4 H 4 H Ddatar% H 4 4 H DKilter H 4
ro? 4 H CO)ERT!$ar%&ar!8(, CO)ERT!$ar%& ar!8(, Dlbo7#d( H 4 ro? 4 H CO)ERT!$ar%&ar!8(, CO)ERT!$ar%& ar!8(, D7bo7#d( ret7r# reJ7eted re%ord
=ou may receive the following error message from SQL Server, which you can confidently ignore7 Ca##ot add ro? to .Jl/de"e#de#%ie Kor t&e tored "ro%ed7re be%a7e it de"e#d o# t&e +ii#= table 4"/ee%7teSGL4. T&e tored
"ro%ed7re ?ill till be %reated &o?e$er, it %a##ot be 7%%eK7ll ee%7ted 7#til t&e table eit.
The util1age procedure accepts @ parameters7 /-atasrc /or-er /or -er+ + /iel-l /ie l-lis is /il ilter /pa&eNum /pa&eN um /pa&e*ie /pa&e* ie
t"e ta!le (or store- proce-ure etc.) name t"e DE DE clause clause t"e iel-s iel-s to return return (inclu (inclu-in -in& & calcul calculateate- epress epression ions) s) t"e ,7EE clau ause se t"e pa&e pa&e to return return (must (must !e !e &reater &reater t"an or or eual to one) one) t"e num!er num!er o recor-s recor-s per pa&e
The stored procedure needs the name of a data source to 4uery against )such as a table* and one or more fields to sort by )since "3(!)* re4uires an "!D(! '= clause*. f ?filter is blan/ )the default*, it will be set to A< B <A as a simple way to select all records. f ?pageSi6e is not supplied, the 4uery will run without paging and will not return a record count. f, however, ?pageSi6e is supplied, a version of the 4uery is eecuted to get the total number of records.and n order have this record count available within the procedure as a to returned value, we use sp$eecuteSQL to support eecuting the statement while returning an output parameter. The record count is used to prevent returning empty results when possible, and to support paging interfaces that calculate the number of pages available )such as 2rid3iew*. f we were calling this stored procedure to populate a 2rid3iew, we would return ?recct as a !eturn3alue parameter instead of using a result set, but we will use a result set for demonstration purposes. The procedure calculates what the actual record positions will be for the re4uested page. !ather than allow the 4uery to fail, there are safety chec/s ensuring that ?pageSi6e and ?page%um are greater than 6ero, and that the result set will not be empty. f the specified page is out of range, this procedure will return the last possible page of records. This is helpful if a user changes more than one setting before refreshing their data, or if a significant amount of data is deleted between re4uests. The remainder of the procedure is virtually identical to the pageSalaries procedure. To test the util102( stored procedure, eecute the following statement7 7til-AE 4Salarie4, 4"ero#4, 4*4, 4i#%o+e 10004, 2, :
(ven though the re4uest should be for records 8@ through 8C - far outside of what is available - the procedure returns the last available page of records. n contrast, re4uesting the third page with seven records per page using7 7til-AE 4Salarie4, 4"ero#4, 4"ero#, i#%o+e4, 44, 3, ;
...returns the last three records, as the page is not completely out of bounds7 person
income
*tep"anie 4#000 *ue
96000
,al-o
4#000
0ll of these eamples eamples are based on simple single-t single-table able 4ueries, which may not reflect what you need in the real world. #hile the util102( procedure does not support ad-hoc "%s, it does wor/ with SQL 3iews. f you want paging support for multi-table 4ueries,
you should create a 3iew )with all of the necessary "%s* to use as the data source. &sing a 3iew follows good design practices as it ensures that your oins are performed consistently, consistently, allows easier adhoc 4uerying from the command line, and is much easier to troubleshoot than a stored procedure;s dynamic S(L(9T statement logic. Conclusion
#hile SQL Server does not have as simple a method for paging results as some other databases, features introduced in the + release have made it possible to page results efficiently more easily than ever before. n the net article in this series, we will go a step further and integrate this paging logic with a 2rid3iew through a Data 0ccess Layer. Layer.