When should keyset pagination replace EF Core Skip/Take for large result sets?
0 reputation · 15 Feb 2020, 09:27 UTC
0 reputation · 15 Feb 2020, 09:27 UTC
When a .NET service paginates a large table through EF Core, Skip() and Take() translate to an OFFSET-FETCH clause on SQL Server (LIMIT/OFFSET on other providers): the database scans and discards every row before the offset before returning the requested page. Take() alone maps to an efficient TOP clause, but the cost of Skip() grows with the page number, so deep pages get progressively more expensive regardless of EF Core version.
Keyset pagination avoids that scan by filtering on an indexed sort column in a WHERE clause, but it changes the API contract: clients can no longer jump to an arbitrary page number, and the sort key must be unique and stable so records are neither duplicated nor skipped between pages.
35275 reputation · 15 Feb 2020, 19:54 UTC
Keyset pagination should replace Skip()/Take() when your application needs to serve pages beyond approximately 10,000–50,000 rows into the result set, assuming a well-indexed sort column. Below this range, OFFSET-FETCH typically performs acceptably; above it, the cost of scanning and discarding rows grows linearly with the page number, creating a measurable bottleneck.
When EF Core translates Skip(n).Take(m), it generates OFFSET n ROWS FETCH NEXT m ROWS ONLY on SQL Server. The query engine must materialize and discard the first n rows before returning the requested page. This happens regardless of whether an index exists on the sort column—the index may be used, but the engine still walks the index to reach the offset. As n increases, so does CPU, I/O, and memory usage.
Keyset pagination filters using a WHERE clause instead of OFFSET. For example:
SELECT * FROM Items
WHERE (CreatedDate, Id) > (@lastDate, @lastId)
ORDER BY CreatedDate, Id
OFFSET 0 ROWS FETCH NEXT 50 ROWS ONLY
This requires:
(SortColumn, PrimaryKey) ensures no duplicates.(SortColumn, PrimaryKey) keeps seeks fast.ORDER BY columns must be used for filtering and retrieval.< vs > predicates.OFFSET-FETCH is generated.EXPLAIN or execution plans for increasing offsets; watch for rising logical reads.If your use case involves deep pagination (pages > 10K rows) or high-frequency access to later pages, migrate to keyset pagination. The switch requires client-side cursor handling but eliminates the offset scan entirely. For random page access or accurate counts, consider a hybrid approach combining keyset with cached totals.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.