Pagination is often used in applications where the user can click Previous/Next to navigate the pages that make up the results, or click on a page number to go directly to a specific page.
When running queries in SQL Server, you can paginate the results by using the OFFSET
and FETCH
arguments of the ORDER BY
clause. These arguments were introduced in SQL Server 2012, therefore you can use this technique if you have SQL Server 2012 or higher.
In this context, pagination is where you divide the query results into smaller chunks, each chunk continuing where the previous finished. For example, if a query returns 1000 rows, you could paginate them so that they’re returned in groups of 100. An application can pass the page number and page size to SQL Server, and SQL Server can then use it to return just the data for the requested page.
Example 1 – No Pagination
First, let’s run a query that returns all rows in a table:
SELECT * FROM Genres ORDER BY GenreId;
Result:
+-----------+---------+ | GenreId | Genre | |-----------+---------| | 1 | Rock | | 2 | Jazz | | 3 | Country | | 4 | Pop | | 5 | Blues | | 6 | Hip Hop | | 7 | Rap | | 8 | Punk | +-----------+---------+
This example uses no pagination – all results are displayed.
This result set is so small that it wouldn’t normally require pagination, but for the purposes of this article, let’s paginate it.
Example 2 – Display the First 3 Results
This example displays the first three results:
SELECT * FROM Genres ORDER BY GenreId OFFSET 0 ROWS FETCH NEXT 3 ROWS ONLY;
Result:
+-----------+---------+ | GenreId | Genre | |-----------+---------| | 1 | Rock | | 2 | Jazz | | 3 | Country | +-----------+---------+
In this case, I specify that the results should start at the first result and display the next three rows. This is done using the following:
OFFSET 0 ROWS
specifies that there should be no offset (an offset of zero).FETCH NEXT 3 ROWS ONLY
gets the next three rows from the offset. Since I specified an offset of zero, the first three rows are fetched.
If all we wanted was the top 3 results, we could have achieved the same result by using the TOP
clause instead of specifying the offset and fetch values. However, this wouldn’t have allowed us to do the next part.
Example 3 – Display the Next 3 Results
Now let’s display the next three results:
SELECT * FROM Genres ORDER BY GenreId OFFSET 3 ROWS FETCH NEXT 3 ROWS ONLY;
Result:
+-----------+---------+ | GenreId | Genre | |-----------+---------| | 4 | Pop | | 5 | Blues | | 6 | Hip Hop | +-----------+---------+
So the only thing I changed was the offset.
The offset and fetching values can also be an expression provided as a variable, parameter, or constant scalar subquery. When a subquery is used, it cannot reference any columns defined in the outer query scope (it can’t be correlated with the outer query).
The following examples use expressions to show two approaches to paginating the results.
Example 4 – Pagination by Row Number
This example uses expressions to specify the row number to start at.
DECLARE @StartRow int = 1, @RowsPerPage int = 3; SELECT * FROM Genres ORDER BY GenreId ASC OFFSET @StartRow - 1 ROWS FETCH NEXT @RowsPerPage ROWS ONLY;
Result:
+-----------+---------+ | GenreId | Genre | |-----------+---------| | 1 | Rock | | 2 | Jazz | | 3 | Country | +-----------+---------+
Here, I use @StartRow int = 1
to specify that the results should start at the first row.
Here’s what happens if I increment that value to 2
.
DECLARE @StartRow int = 2, @RowsPerPage int = 3; SELECT * FROM Genres ORDER BY GenreId ASC OFFSET @StartRow - 1 ROWS FETCH NEXT @RowsPerPage ROWS ONLY;
Result:
+-----------+---------+ | GenreId | Genre | |-----------+---------| | 2 | Jazz | | 3 | Country | | 4 | Pop | +-----------+---------+
It starts at the second row. Using this method, I can specify the exact row to start at.
Example 5 – Pagination by Page Number
This example is almost identical to the previous example, except that it allows you to specify the page number, as opposed to the row number.
DECLARE @PageNumber int = 1, @RowsPerPage int = 3; SELECT * FROM Genres ORDER BY GenreId ASC OFFSET (@PageNumber - 1) * @RowsPerPage ROWS FETCH NEXT @RowsPerPage ROWS ONLY;
Result:
+-----------+---------+ | GenreId | Genre | |-----------+---------| | 1 | Rock | | 2 | Jazz | | 3 | Country | +-----------+---------+
So the first result is the same. However, let’s see what happens when we increment @PageNumber
to 2
(I renamed this variable to reflect its new purpose).
DECLARE @PageNumber int = 2, @RowsPerPage int = 3; SELECT * FROM Genres ORDER BY GenreId ASC OFFSET (@PageNumber - 1) * @RowsPerPage ROWS FETCH NEXT @RowsPerPage ROWS ONLY;
Result:
+-----------+---------+ | GenreId | Genre | |-----------+---------| | 4 | Pop | | 5 | Blues | | 6 | Hip Hop | +-----------+---------+
This time the results start at the fourth row. So using this method you can simply pass the page number rather than the row number.
Example 6 – Pagination Loop
To finish off, here’s a quick example that loops through all pages and specifies the starting row number for each iteration:
DECLARE @StartRow int = 1, @RowsPerPage int = 3; WHILE (SELECT COUNT(*) FROM Genres) >= @StartRow BEGIN SELECT * FROM Genres ORDER BY GenreId ASC OFFSET @StartRow - 1 ROWS FETCH NEXT @RowsPerPage ROWS ONLY; SET @StartRow = @StartRow + @RowsPerPage; CONTINUE END;
Result:
+-----------+---------+ | GenreId | Genre | |-----------+---------| | 1 | Rock | | 2 | Jazz | | 3 | Country | +-----------+---------+ (3 rows affected) +-----------+---------+ | GenreId | Genre | |-----------+---------| | 4 | Pop | | 5 | Blues | | 6 | Hip Hop | +-----------+---------+ (3 rows affected) +-----------+---------+ | GenreId | Genre | |-----------+---------| | 7 | Rap | | 8 | Punk | +-----------+---------+ (2 rows affected)
Example 7 – ROW vs ROWS
If you encounter code that uses ROW
instead of ROWS
, both arguments do the same thing. They are synonyms and are provided for ANSI compatibility.
Here’s the first example on this page, but with ROW
instead of ROWS
.
SELECT * FROM Genres ORDER BY GenreId OFFSET 0 ROW FETCH NEXT 3 ROW ONLY;
Result:
+-----------+---------+ | GenreId | Genre | |-----------+---------| | 1 | Rock | | 2 | Jazz | | 3 | Country | +-----------+---------+
Example 8 – FIRST vs NEXT
The same applies to FIRST
and NEXT
. These are synonyms provided for ANSI compatibility.
Here’s the previous example but with FIRST
instead of NEXT
.
SELECT * FROM Genres ORDER BY GenreId OFFSET 0 ROW FETCH FIRST 3 ROW ONLY;
Result:
+-----------+---------+ | GenreId | Genre | |-----------+---------| | 1 | Rock | | 2 | Jazz | | 3 | Country | +-----------+---------+