I looked at various ways to write a stored procedure to return a "page" of data. This was for use with ASP ObjectDataSource , but it could be considered as a more general problem.
Requirement - return a subset of data based on the usual parameters of the search call; startPageIndex and maximumRows , but also the sortBy parameter to sort the data. There are also some parameters that are passed to filter data in various conditions.
One common way to do this is something like this:
[Method 1]
;WITH stuff AS ( SELECT CASE WHEN @SortBy = 'Name' THEN ROW_NUMBER() OVER (ORDER BY Name) WHEN @SortBy = 'Name DESC' THEN ROW_NUMBER() OVER (ORDER BY Name DESC) WHEN @SortBy = ... ELSE ROW_NUMBER() OVER (ORDER BY whatever) END AS Row, ., ., ., FROM Table1 INNER JOIN Table2 ... LEFT JOIN Table3 ... WHERE ... (lots of things to check) ) SELECT * FROM stuff WHERE (Row > @startRowIndex) AND (Row <= @startRowIndex + @maximumRows OR @maximumRows <= 0) ORDER BY Row
One of the problems is that it does not give a general account, and for this we need another stored procedure. This second stored procedure should replicate the parameter list and complex WHERE . Not nice.
One solution is to add an extra column to the final selection list (SELECT COUNT (*) FROM stuff) AS TotalRows . This gives us the total, but repeats it for each row in the result set, which is not ideal.
[Method 2]
Here's an interesting alternative ( http://www.4guysfromrolla.com/articles/032206-1.aspx ) using dynamic SQL. He believes that performance is better, because the CASE statement in the first solution is addictive. Fair enough, and this solution simplifies getting totalRows and spanks it into an output parameter. But I hate dynamic SQL coding. All that "bit SQL" + STR (@ parm1) + "bit bigger than SQL" gubbins.
[Method 3]
The only way I can find to get what I want, without repeating the code that should have been synchronized, and storing things that can be readable, is to return to the βoldβ way of using the table variable:
DECLARE @stuff TABLE (Row INT, ...) INSERT INTO @stuff SELECT CASE WHEN @SortBy = 'Name' THEN ROW_NUMBER() OVER (ORDER BY Name) WHEN @SortBy = 'Name DESC' THEN ROW_NUMBER() OVER (ORDER BY Name DESC) WHEN @SortBy = ... ELSE ROW_NUMBER() OVER (ORDER BY whatever) END AS Row, ., ., ., FROM Table1 INNER JOIN Table2 ... LEFT JOIN Table3 ... WHERE ... (lots of things to check) SELECT * FROM stuff WHERE (Row > @startRowIndex) AND (Row <= @startRowIndex + @maximumRows OR @maximumRows <= 0) ORDER BY Row
(Or a similar method using the IDENTITY column in a table variable). Here I can simply add SELECT COUNT to the table variable to get totalRows and put it in the output parameter.
I did some tests and with a fairly simple version of the query (without sortBy and no filter), method 1 seems to appear on top (almost twice as fast as the other 2). Then I decided to check, I probably needed complexity, and I need SQL stored in stored procedures. With this, I get method 1 taking almost twice as much as the other 2 methods. Which seems strange.
Is there a good reason why I should not reject CTE and stick to method 3?
UPDATE - March 15, 2012
I tried to adapt method 1 to unload the page from CTE into a temporary table so that I could extract TotalRows and then select only the appropriate columns for the result set. This seemed to significantly increase the time (more than I expected). I should add that I run this on a laptop with SQL Server Express 2008 (all I have), but still the comparison should be valid.
I looked again at the dynamic SQL method. Turns out I really didn't do it right (just concatenating strings together). I installed it, as in the documentation for sp_executesql (with a description string for the parameters and a list of parameters), and this is much more readable. Also this method works the fastest in my environment. Why is this still puzzling me, but I think the answer is outlined in Hogan's comment.