• subscribe
December 20, 2004 12:00 AM

Implementing Paging

Let users navigate to adjacent and non-adjacent pages
SQL Server Pro
InstantDoc ID #44138
Downloads
44138.zip

When you write applications that return query results to the client and display the results on the screen, you need to take into account the end user's screen-size limitations. If a query's result set contains too many rows to fit on the screen, you have to introduce logic into the application to split the original result set into chunks or pages. You then provide buttons or other graphical elements that let the user navigate between the result pages. The process of splitting the data into chunks, called paging, is very common, especially with Web applications. Let's look at some efficient ways to achieve paging, using two navigation options and paging techniques, then consider a commonly used technique that isn't guaranteed to work as you might expect.

Orders Scenario
When you're planning to implement paging in your application, you need to determine three things. First, what sort order do you need for the rows you'll return? Second, how many rows do you want to return per page? And third, what navigation technique will you provide for users to page through the results?

The sort order and the page size (i.e., the number of rows in a page) you choose determine which rows appear on which page. My examples use the Northwind database's Orders table. For this article's examples, I use a sort order based on OrderDate and OrderID and a page size of 10 rows. Note that sorting on unique columns is important. Without a unique sort, the relationship of the source row to the target page isn't deterministic, meaning that you can't determine for sure which page a row belongs to. Let's say you want to return the orders sorted chronologically. Because the values in the OrderDate column don't have to be unique, you need to choose a "tiebreaker" column to sort on secondarily that will determine the order of rows that have the same order date. I chose the OrderID column as the tiebreaker because it's unique.

How you choose to set up navigation between pages depends on the application users' needs. If the users need to navigate only from the current page to either the next or the previous page, you can implement a simple, efficient solution. But if users need more dynamic capabilities, such as navigating to a specific page number, you need to implement a solution that lets you filter the rows of a desired page based only on two inputs: page size and page number. Let's start by looking at navigation between adjacent pages.

Navigating to Adjacent Pages
To allow navigation to adjacent pages, you need to implement code that performs three tasks: get the first page, get the next page, and get the previous page. I recommend that you use stored procedures, one for each task. Using stored procedures gives you the flexibility to change your implementation if you find a more efficient paging method later without needing to change the client application.

Run Listing 1's code in the Northwind database to create the stored procedure that returns the first page of the Orders table. The stored procedure's code is simple. The query retrieves the top 10 rows, based on an ascending sort by order date and order ID. Run the following code to test the stored procedure:

EXEC dbo.usp_GetFirstPage;

As Figure 1 shows, this code returns the first page of Orders.

The stored procedure that returns the next page after the current one accepts two input arguments: the order date and order ID of the last row on the previously returned page. In the client application, you need to store those values locally while processing the call to the current page.

Run the code that Listing 2, page 38, shows to create the usp_GetNextPage stored procedure, which returns the page after the current one. The query filters to find the rows that have either a later order date than the order date of the last row on the previous page or the same order date and a higher order ID. The query returns the top 10 rows based on an ascending sort by order date and order ID.

Assuming the user has already requested the first page, the client application locally stored the values of order date and order ID from the last row on the first page returned. When the user requests the second page, the client application submits the following code to invoke usp_GetNextPage:

EXEC dbo.usp_GetNextPage @last_orderdate = '19960716', @last_orderid   = 10257;

The procedure returns the second page of Orders, as the result in Figure 2 shows. The client application now locally stores the order date and order ID of the last row on this page in anticipation of sending those values as input arguments if the user requests the next page. Similarly, the client application also locally stores the order date and order ID of the first row returned on this page, which it will use if the user requests the previous page.

The implementation of the stored procedure that returns the previous page is similar to the implementation of usp_GetNextPage, with a few changes. First, you need to reverse the direction of the operators—instead of greater than (>), use less than (

Run the code that Listing 3 shows to create the usp_GetPrevPage stored procedure. To test the stored procedure, execute it as follows, providing the order date and the order ID of the first row returned on the second page:

EXEC dbo.usp_GetPrevPage @first_orderdate = '19960717', @first_orderid = 10258;

You get the first page (Figure 1) back again.



ARTICLE TOOLS

Comments
  • Bob
    6 years ago
    Jul 15, 2006

    asdfadsf

  • BG_SQL
    7 years ago
    May 17, 2005

    I’d like to point out a very important knowledge base article that was revised after I opened a case with Microsoft to clarify the confusion surrounding IDENTITY issues. I strongly recommend going over it as it has many implications related to paging in SQL Server 2000.
    Here’s the link to the KB article (273586) The behavior of the IDENTITY function when used with SELECT INTO or INSERT .. SELECT queries that contain an ORDER BY clause:

    http://support.microsoft.com/default.aspx?scid=kb;en-us;273586

    Regards,
    Itzik Ben-Gan

  • BG_SQL
    8 years ago
    Dec 21, 2004

    Andy,

    You can clear the data cache using DBCC DROPCLEANBUFFERS. Also, make sure you have an appropriate index in place for the paging technique that I suggested to work fast. An ideal index would be a covering one on (sort_columns, covered_columns), but if you cannot afford one, you can make it a non-covering index on sort_columns only. Just be aware that each target row would cause a lookup, which is much slower and I/O consuming than the covering index.

    As for the anonymous user that requested an example, here’s one using SELECT INTO:

    set nocount on
    use tempdb
    go
    drop table ta, tb, #t
    go
    create table ta(a int not null)
    insert into ta values(3)
    insert into ta values(1)
    insert into ta values(2)

    create table tb(b int not null primary key)
    insert into tb values(4)
    insert into tb values(1)
    insert into tb values(3)
    insert into tb values(2)

    select b, identity(int, 1, 1) as rn
    into #t
    from ta join tb on a = b
    order by b

    select * from #t order by b

    -- Output:
    b rn
    ----------- -----------
    1 2
    2 3
    3 1

    -- Plan:
    |--Table Insert(OBJECT:([#t]), SET:([#t].[rn]=[Expr1005],
    [#t].[b]=[tb].[b]))
    |--Top(ROWCOUNT est 0)
    |--Compute Scalar(DEFINE:([Expr1005]=setidentity([Expr1004], -7, 0,
    '#t')))
    |--Sort(ORDER BY:([tb].[b] ASC))
    |--Compute Scalar(DEFINE:([Expr1004]=getidentity(-7, 0, '#t')))
    |--Nested Loops(Inner Join, OUTER REFERENCES:([ta].[a]))
    |--Table Scan(OBJECT:([tempdb].[dbo].[ta]))
    |--Clustered Index
    Seek(OBJECT:([tempdb].[dbo].[tb].[PK__tb__522A84E8]),
    SEEK:([tb].[b]=[ta].[a]) ORDERED FORWARD)

    I’m afraid I currently don’t have an example for INSERT INTO, but I’ll keep looking for one.

    Regards,
    Itzik Ben-Gan

  • Anonymous User
    8 years ago
    Dec 20, 2004

    Knowledge Base article ID 273586 seems very pertinent. The article confirms the wisdom of Itzik's warnings about using the SELECT INTO syntax. However, the article does not mention the INSERT SELECT syntax. This could lead one to believe that the two situations are handled differently, and that the INSERT SELECT syntax is not subject to the same problems. There does not seem to be a KB article advising against depending upon the INSERT SELECT syntax to behave as commonly expected.

  • Anonymous User
    8 years ago
    Dec 19, 2004

    Itzik,

    Thanks for sharing the fact that a case exists where the Sort operation followed the Compute Scalar operation. Are you at liberty to say which syntax the case used? If so, did the case use SELECT...INTO or INSERT...SELECT (not that the syntax would necessarily make any difference)?

You must log on before posting a comment.

Are you a new visitor? Register Here