SQL performance: WHERE vs WHERE(ROW_NUMBER)

As other already pointed out, the queries return different results and are comparing apples to oranges.

But the underlying question remains: which is faster: keyset driven paging or rownumber driven paging?

Keyset Paging

Keyset driven paging relies on remembering the top and bottom keys of the last displayed page, and requesting the next or previous set of rows, based on the top/last keyset:

Next page:

select top (<pagesize>) ...
from <table>
where key > @last_key_on_current_page
order by key;

Previous page:

select top (<pagesize>)
from <table>
where key < @first_key_on_current_page
order by key desc;

This approach has two main advantages over the ROW_NUMBER approach, or over the equivalent LIMIT approach of MySQL:

  • is correct: unlike the row number based approach it correctly handles new entries and deleted entries. Last row of Page 4 does not show up as first row of Page 5 just because row 23 on Page 2 was deleted in the meantime. Nor do rows mysteriously vanish between pages. These anomalies are common with the row_number based approach, but the key set based solution does a much better job at avoiding them.
  • is fast: all operations can be solved with a fast row positioning followed by a range scan in the desired direction

However, this approach is difficult to implement, hard to understand by the average programmer and not supported by the tools.

Row Number Driven

This is the common approach introduced with Linq queries:

select ...
from (
  select ..., row_number() over (...) as rn
  from table)
where rn between @firstRow and @lastRow;

(or a similar query using TOP)
This approach is easy to implement and is supported by tools (specifically by Linq .Limit and .Take operators). But this approach is guaranteed to scan the index in order to count the rows. This approach works usually very fast for page 1 and gradually slows down as the an one goes to higher and higher page numbers.

As a bonus, with this solution is very easy to change the sort order (simply change the OVER clause).

Overall, given the ease of the ROW_NUMBER() based solutions, the support they have from Linq, the simplicity to use arbitrary orders for moderate data sets the ROW_NUMBER based solutions are adequate. For large and very large data sets, the ROW_NUMBER() can occur serious performance issues.

One other thing to consider is that often times there is a definite pattern of access. Often the first few pages are hot and pages after 10 are basically never viewed (eg. most recent posts). In this case, the penalty that occurs with ROW_NUMBER() for visiting bottom pages (display pages for which a large number of rows have to be counted to get the starting result row) may be well ignored.

And finally, the keyset pagination is great for dictionary navigation, which ROW_NUMBER() cannot accommodate easily. Dictionary navigation is where instead of using page number, users can navigate to certain anchors, like alphabet letters. Typical example being a contact Rolodex like sidebar, you click on M and you navigate to the first customer name that starts with M.

Leave a Comment