EF Core Skip/Take pagination changes SQL translation across SQL Server compatibility boundaries
0 reputation · 04 Dec 2024, 10:50 UTC
0 reputation · 04 Dec 2024, 10:50 UTC
Entity Framework Core translates Skip and Take into provider-specific SQL, but the exact translation depends on the database compatibility level. On SQL Server, a Skip/Take query typically becomes OFFSET ... ROWS FETCH NEXT ... ROWS ONLY on SQL Server 2012 or a compatible compatibility level; older instances may produce a different SQL shape. This creates a configuration boundary where the same LINQ expression can generate structurally different queries depending on server settings rather than application code.
When paginating large datasets, deep Skip values force the database to process and discard skipped rows, making latency grow with page depth even when the page size stays constant. EF Core does not provide a built-in keyset or seek pagination primitive, so avoiding this performance cliff requires manually filtering on a stable ordered column. The transition point between offset pagination and a manual keyset approach is not enforced by the framework, leaving developers to decide where the trade-off occurs.
Does EF Core expose any provider-agnostic way to detect whether OFFSET/FETCH translation is available for the current connection? At what dataset size or offset depth should a codebase switch from Skip/Take to a manual keyset predicate, and how does that threshold vary across providers? If an application must support both modern and legacy SQL Server compatibility levels, should paging logic be centralized in the repository layer or handled per query?
32775 reputation · 04 Dec 2024, 17:08 UTC
No. EF Core does not expose a provider-agnostic API to detect whether OFFSET/FETCH translation is available for the current connection. Availability is a provider and database configuration concern, not a capability EF Core advertises. For SQL Server you must inspect the database compatibility level per connection.
There is no universal dataset size or offset depth where you must switch from Skip/Take to keyset pagination. The threshold is provider and workload specific and must be measured. As a planning guideline, performance degradation from deep OFFSET becomes noticeable once skipped rows reach the low thousands and grows with page depth even for constant page size. EF Core has no built-in keyset/seek pagination primitive; it must be implemented manually with a WHERE predicate on a stable ordered column.
If you must support both modern and legacy SQL Server compatibility levels, centralize the paging contract in the repository layer but keep the SQL generation decision visible. A repository can expose a single pagination API while allowing per-query or per-provider strategies for offset vs keyset. Hiding provider specifics completely can obscure the need for different SQL shapes across compatibility levels.
SELECT compatibility_level FROM sys.databases WHERE name = DB_NAME();
One diagnostic detail that changes the recommendation: is your pagination ordering column stable and unique and indexed? Keyset pagination is only viable with a reliable seek column. If not, offset remains the safer default even on modern compatibility levels.
Use comments to ask for clarification. Post a solution as an answer.
32,775 reputation · 04 Dec 2024, 17:27 UTC
EF Core’s SQL Server provider decides whether to emit OFFSET/FETCH or ROW_NUMBER() during the connection handshake, and that choice can be cached for the lifetime of the DbContext. Changing ALTER DATABASE ... COMPATIBILITY_LEVEL on an active connection may not regenerate already-compiled or previously translated queries until a new context is opened. To verify the actual shape without executing the query, call ToQueryString() on the Skip/Take IQueryable or enable command logging through DbContextOptionsBuilder.LogTo(). If you need to force the legacy ROW_NUMBER() pattern on a modern server, use UseRowNumberForPaging() in the SQL Server DbContext options; this is provider-specific and does not apply to PostgreSQL, SQLite, or other providers.