Scott on Writing

Musings on technical writing...

Paging Done RIGHT (in SQL Server 2000 & 2005)

In an earlier article of mine, Custom Paging in ASP.NET 2.0 with SQL Server 2005, I wrote about SQL Server 2005's new ROW_NUMBER() function, which makes efficiently returning a paged result in SQL Server 2005 relatively easy (blog entry). While the SQL Server 2005 technique is neat, not everyone has migrated to SQL Server 2005, and a number of 4Guys readers had asked for an article that examined efficiently paging results with SQL Server 2000. So I wrote the horribly mis-titled article, Efficiently Paging Through Large Results Sets in SQL Server 2000, which showcased the technique I've always used for implementing custom paging in ASP.NET 1.x.

In a word, it is the “wrong” way, as my technique, while more efficient than simply grabbing all of the records blindly as in default paging, is still more than an order of magnitude worse in performance than an improved version. My slow approach used a stored procedure that created a table variable into which the entire contents of the table to be paged through were dumped. This created a “synthetic ID” that could be used to correctly grab just the correct page of data. The inefficiency stemmed from the fact that all of the table's data was being poured into this table variable, which could be prohibitively slow for result sets with as little as tens of thousands of records.

After publishing the article I quickly started receiving emails from folks helpfully pointing out that my technique could be vastly improved upon by using SET ROWCOUNT to reduce the number of records that needed to be injected into the table variable. Using SET ROWCOUNT, you can instruct SQL Server to stop processing a query after a certain number of records have been processed. To retrieve the mth page of data when showing n records per page, one could simply grab the first m*n records and then return the last n records in that subset. When m is sufficiently small enough (that is, when retrieving small page numbers... 1, 2, 10, 15, etc.), m*n is likely to be much smaller than the total number of records in the table, thereby greatly improving performance. (It's a tad embarassing that I've overlooked ROWCOUNT for all these years, seeing as I've written an article on it in the past...)

I was planning on writing an article on this improvement when I got an even more clever suggestion from Greg Hamilton. Greg showed how the table variable could be dispensed of altogether. Rather than move all m*n records into a table variable, just compute the ID of record (m-1)*n+1. This is the ID of the record at the start of page m. Then, use SET ROWCOUNT to just get the next n records after (and including) that ID. The clever part was in efficiently finding the ID value of the (m-1)*n+1 record. Greg did this by using a query like so:

DECLARE @first_id int

SET ROWCOUNT @startRowIndex
SELECT @first_id = ID_Field FROM Table

Such a query assigns the last value of ID_Field in the result set to @first_id. Since the SET ROWCOUNT limits the result set size to the starting row index, the last record returned in the first record of the page to retrieve. Once this value has been obtained, the following query returns the appropriate page of data:

SET ROWCOUNT @recordsPerPage

SELECT ... FROM Table
WHERE ID_Field >= @first_id

Pretty slick! For more on Greg's technique, along with some performance comparisons, check out his article, A More Efficient Method for Paging Through Result Sets.

Learning that my approach that I've used for years had much to be improved upon imparts three important lessons:

  1. Never assume that your approach/code/algorithm is optimal. There may be (and likely is!) some way in which it can be optimized or better-tuned.
  2. Due to the breadth of technologies and concepts a web developer must be familiarized with (ASP.NET, SQL, client/server model, client-side script, etc., etc.), it's easy to overlook an important aspect on one area that can greatly affect performance or other areas of usability.
  3. No matter how much you may know (or think you know :-), there's always new things to learn. I guess this is part of what makes this job so fun - there's always something new to learn!

posted on Tuesday, April 25, 2006 1:25 PM

Feedback

# re: Paging Done RIGHT (in SQL Server 2000 & 2005) 4/25/2006 6:23 PM Brian Boatright

Those results are incredible. His solution is one of those "why didn't I think of that".

# re: Paging Done RIGHT (in SQL Server 2000 & 2005) 4/26/2006 12:27 AM Josh

Great article - particularly interesting about the optimizer!

Maybe I've missed something here - but doesn't the final solution only work if you are paging the results sorted by their identity column.

What if you want to page through the results sorted by Employee Surname, for example?

# re: Paging Done RIGHT (in SQL Server 2000 & 2005) 4/26/2006 7:11 AM Scott Mitchell

Josh, the author left a bit of a teaser in his article about extending the script to support sorts by other columns. I believe he's currently working/planning on an article that follows up on his original and examines how to add the sorting capabilities.

# Pagina 4/26/2006 1:50 PM LA.Net

# re: Paging Done RIGHT (in SQL Server 2000 & 2005) 4/28/2006 4:35 AM Rob

Good article - definitely got me thinking about how I can improve some of my slower sprocs!

In addition to the sorting limitation, it seems using this method means you can't get at the total result count, which is usually required for paging navigation (eg showing 1-10 of 73). Hopefully someone will prove me wrong on that, but at the moment it's a show-stopper.

# re: Paging Done RIGHT (in SQL Server 2000 & 2005) 4/28/2006 9:02 AM Scott Mitchell

Rob, you can always get the total number of records from a table using COUNT(*), such as:

SELECT COUNT(*) FROM TableName


And you can include WHEREs in there. Without a WHERE, it will use the clustered index, and with a WHERE it can use an index if the columns in the where clause are indexed. In short, it can quickly compute the total number of columns in the table.

# re: Paging Done RIGHT (in SQL Server 2000 & 2005) 4/30/2006 1:52 AM PJ

Thats a cool idea!!

However I am a bit confused about the way to to find the first_id, let's say for example when looking for search results should the portion of the query be

SET ROWCOUNT @startRowIndex
SELECT @first_id = ID_Field FROM FREETEXT (column, @Search)

and the search query itself...

instead of this ..

SET ROWCOUNT @startRowIndex
SELECT @first_id = ID_Field FROM Table

Have I understood it right? If so wouldn't that result in dual read, maybe 3 reads if the TotalRecord Count is requested for each page?

# re: Paging Done RIGHT (in SQL Server 2000 & 2005) 5/2/2006 8:24 AM Rob

Hi Scott,

I'm after the count of matches to my query, not the whole table. So in effect, I'd have to run my query twice - once to get the result 'page' and again to get the count. Is that right?

My query involves multiple joins and a full text query, so the cost of doing this twice is surely much higher than the temp table technique.




# re: Paging Done RIGHT (in SQL Server 2000 & 2005) 5/8/2006 2:05 PM Mustafa

Hallooz there...
I've applied tht same solution myself... and its yet the best and the simplest way one can do...

I was using thz for the ASP.net Repeaters to bring only the required rows, and when i want to get the next page rows, i simply take the last ID fom the repeater.DataSource and send it to the stored procedure...

But one thing i hav missed... How can i get the PREVIOUS page!???

I figured tht i should add new parameter to check that the query should be go back or forth...

ex.
CREATE PROCEDURE PagingTest
@PageSize INT,
@FirstID,
@GoNext BIT

AS

SET ROWCOUNT @PageSize

IF (@GoNext=1)
SELECT ... FROM Table
WHERE ID_Field > @FirstID
ORDER BY ID_Field ASC
ELSE
SELECT ... FROM Table
WHERE ID_Field < @FirstID
ORDER BY ID_Field DESC

but as u can see its a bit lousy one! :(
the previous page will hav the results reversed!!! :S

If i only could get the ID_Field using a Row Index or number from the database, thingz would be MUCH easier!!!

ANY help plz????

# re: Paging Done RIGHT (in SQL Server 2000 & 2005) 5/8/2006 2:19 PM Mustafa

And oh! Btw the way... As a solution for your problem "Rob"... No need for 2 round trips to the database... Here is the sol:

---------------------------------------------
DECLARE @NumOfRows INT

SELECT @NumOfRows= COUNT (ID_Field) FROM Table

SELECT ...,@NumOfRows 'NumOfRows' FROM Table
WHERE ID_Field > @FirstID
---------------------------------------------

thz way, the result table will hav an extra column named "NumOfRows" with the number of rows inside it!

Better, right?

# re: Paging Done RIGHT (in SQL Server 2000 & 2005) 5/8/2006 2:29 PM Mustafa

Ooh... Sorry Rob, i didnt fully read your comment, excuse me, its 1:00AM n here and im working & sleepy! My eyes are BLURING from the monitor... But i think my solution of adding a new column would solve your problem well...

Hope someone will help me in my problem...

# re: Paging Done RIGHT (in SQL Server 2000 & 2005) 5/12/2006 9:08 AM Mustafa

It seems that i didnt read all the article right! :S
All my roblems are solved now... thanx...

# re: Paging Done RIGHT (in SQL Server 2000 & 2005) 5/13/2006 1:36 PM Diego

Hi Scott,

Thanks for this usefull solution!!
ITS REALLY REALLY GREAT!!

But Im wondering the same thing as ROB...
How can I get the recordcount to display the final user the total qty of results?

Only solution I see is run the select twice :(

Regards,
Diego

PS: sorry for my English, my native tongue is Spanish

# re: Paging Done RIGHT (in SQL Server 2000 & 2005) 5/17/2006 10:40 AM Ian Murphy

Take a look at the following article on codeproject by a guy called Jasmin Muharemovic

http://www.codeproject.com/aspnet/PagingLarge.asp

I haven't compared your tecnique with his evaluation but his article is worth a look.

Ian

# blogging and forum posting helps 8/11/2006 4:55 AM jokiz's blog

i found this post of scott mitchell wherein one of his blog post was corrected by one of its readers.

# re: Paging Done RIGHT (in SQL Server 2000 & 2005) 1/9/2007 3:23 PM dejan

Any work arounds if the query is actually dynamic sql, because you run into scope issues and can't assign it to a temp table / table var?

eg
select @sql_string = 'select mjh.job_id'
+ ' ,mj.job_description'
+ ' ,mjh.load_id'
+ ' ,mjh.job_history_id'
+ ' ,ede.data_error_id'
+ ' ,mjh.start_date_time'
+ ' ,ede.etl_error_message '
+ 'from mct_job_history mjh, mct_jobs mj, etl_data_error ede '
+ 'where ede.mct_job_history_id = mjh.job_history_id and '
+ ' mjh.job_id = mj.job_id and '
+ dbo.fn_etl_build_sql_condition('mjh.job_history_id', 0, @in_job_history_id, 0, '=', 1)
+ dbo.fn_etl_build_sql_condition('mjh.load_id', 0, @in_load_id, 1, '=', 1)
+ dbo.fn_etl_build_sql_condition('mjh.job_id', 0, @in_job_id, 1, 'like', 1)
+ dbo.fn_etl_build_sql_condition('mjh.start_date_time', 0, @in_start_from_date, 1, '>=', 1)
+ dbo.fn_etl_build_sql_condition('mjh.end_date_time', 0, @in_start_to_date, 1, '<=', 1)

# re: Paging Done RIGHT (in SQL Server 2000 & 2005) 4/3/2007 7:14 PM Trevor Saint

Hi guys, could use a little help if thats ok. I love this SP, as its so close to being perfect.

For my SP i have the following ready:-

CREATE PROCEDURE spManagementPaging

@first_id int,
@recordsPerPage int,
@startRowIndex int,
@NumOfRows int

AS

SET ROWCOUNT @startRowIndex
SELECT @first_id = managementID FROM tblManagement

SET ROWCOUNT @recordsPerPage
SELECT managementID, managementFirstname, managementSurname, managementJobRole, managementEmail, managementOrder, managementActive
FROM tblManagement
WHERE managementID >= @first_id

But I am struggling to introduce the count feature "NumOfRows" As in order to determin the last record, there is no way of determining if your at the end of the recordset.

I am sorry for lack of understanding, but am picking stuff up gradually ;)

Kind regards

Trev

Title:  
Name:  
Url:
Protected by Clearscreen.SharpHIPEnter the code you see:
Comments   

Add To Your Reader

My Links

Archives

Post Categories

 

I am a Microsoft MVP for ASP.NET.
I am an ASPInsider.
<May 2008>
SMTWTFS
27282930123
45678910
11121314151617
18192021222324
25262728293031
1234567

Comment Stats

DayTotal% of Total
Sunday 1866.8%
Monday 37913.9%
Tuesday 45316.7%
Wednesday 50418.5%
Thursday 53519.7%
Friday 49418.2%
Saturday 1666.1%
Total 2717100.0%

Hour1Total% of Total
12:00 AM 652.4%
1:00 AM 682.5%
2:00 AM 622.3%
3:00 AM 742.7%
4:00 AM 572.1%
5:00 AM 1033.8%
6:00 AM 1084.0%
7:00 AM 1585.8%
8:00 AM 1716.3%
9:00 AM 1475.4%
10:00 AM 1716.3%
11:00 AM 1816.7%
12:00 PM 1886.9%
1:00 PM 1696.2%
2:00 PM 1605.9%
3:00 PM 1324.9%
4:00 PM 1073.9%
5:00 PM 923.4%
6:00 PM 913.3%
7:00 PM 963.5%
8:00 PM 833.1%
9:00 PM 782.9%
10:00 PM 792.9%
11:00 PM 772.8%
Total 2717100.0%

Comments by Blog Entry Date/Time

Day Entry MadeAvg.Total
Sunday 5.54144
Monday 5.22339
Tuesday 4.28419
Wednesday 7.67637
Thursday 6.90607
Friday 5.48411
Saturday 5.33160
Total 5.842717

Hour1 Entry MadeAvg.Total
12:00 AM 5.0035
1:00 AM 1.002
5:00 AM 0.000
7:00 AM 7.0035
8:00 AM 5.35107
9:00 AM 6.32278
10:00 AM 6.47246
11:00 AM 4.41181
12:00 PM 6.88330
1:00 PM 3.00111
2:00 PM 5.41222
3:00 PM 8.64285
4:00 PM 4.0589
5:00 PM 5.92154
6:00 PM 4.52113
7:00 PM 9.67174
8:00 PM 9.80147
9:00 PM 5.05111
10:00 PM 5.4265
11:00 PM 4.5732
Total 5.842717

Learn More About Comment Stats
1 - All times GMT -8...


Blog Stats

Favorite Web Sites

My Books

My MSDN Articles