The query uses a row limiting operation without OrderBy warning in EF Core pagination
0 reputation · 10 Sept 2025, 20:42 UTC
0 reputation · 10 Sept 2025, 20:42 UTC
When paginating large datasets in EF Core, the framework emits a warning — "The query uses a row limiting operation (Skip/Take) without OrderBy which may lead to unpredictable results" — whenever Skip or Take is applied without an explicit ordering clause. This warning signals a correctness risk, yet it does not prevent execution.
The core decision is whether to adopt offset-based pagination (Skip/Take) with mandatory OrderBy, accepting linear performance degradation as page depth increases, or to implement keyset pagination manually using a Where predicate on the last-seen key. Keyset pagination avoids the scan-and-discard cost but requires a unique, immutable, sequentially ordered column and careful handling of composite keys. EF Core 8's AsSplitQuery improves include-heavy queries but still generates OFFSET/FETCH for Skip/Take. Client-side evaluation before paging triggers OutOfMemoryException on large result sets, a documented failure mode.
35775 reputation · 11 Sept 2025, 04:44 UTC
The EF Core warning "The query uses a row limiting operation (Skip/Take) without OrderBy" fires whenever paging operators appear without an explicit OrderBy clause. It does not throw an exception, but it marks the result set as potentially non‑deterministic: row ordering can vary across executions, especially after data changes, index rebuilds, or when the query planner chooses different access paths.
Offset‑based pagination scans and discards N × pageSize rows to reach page N. On a table with 1 million rows and a 50‑row page, page 100 scans roughly 500,000 rows before returning results. Performance degrades linearly with page depth.
Keyset (seek) pagination replaces Skip/Take with a WHERE predicate on the last seen key and an ORDER BY of that key. It avoids scanning skipped rows, but requires the ordering column to be unique, immutable, and sequentially ordered. Composite keys need a consistently defined total‑order expression, such as ORDER BY created_at, id.
Globally unique identifiers that are not generated sequentially (e.g., NEWID() or random UUIDs) produce poor offset pagination performance: the database must jump across index pages at irregular intervals, causing high I/O and unpredictable latency. Keyset pagination cannot use a non‑sequential GUID directly unless it is guaranteed unique and the query always sorts by it; even then, the WHERE id > @last_id predicate may skip or reorder rows if gaps or updates exist. A practical compromise is to maintain a separate, monotonically increasing integer or date column for pagination while keeping the GUID as the primary key.
When paginating entities whose natural key is composite, define a deterministic sort expression that totals the key components. Example: ORDER BY created_at DESC, entity_id ASC. The WHERE clause for the next page should mirror this expression exactly, e.g., WHERE created_at < @last_created_at OR (created_at = @last_created_at AND entity_id < @last_entity_id). Any deviation introduces ordering instability, causing missed or duplicated rows.
The Skip/Take‑without‑OrderBy warning has been present since EF Core 7 and continues in 8 and 9. EF Core 8's AsSplitQuery improves query splitting for Include‑heavy graphs but does not alter the OFFSET/FETCH semantics of Skip/Take, so the warning and its performance characteristics remain. Automated log parsers should treat the warning as a consistent signal across these versions, but verify the exact message string per version if filtering alerts.
// Observe the warning in console/log during query translation
var query = context.Set<Entity>().Where(e => e.IsActive).Skip(1).Take(1); // no OrderBy
// Execute and check output for the warning string
// Compare offset vs keyset performance on a large table with an indexed sequential column
var offsetQuery = context.Set<Entity>()
.OrderBy(e => e.SeqId)
.Skip(page * pageSize)
.Take(pageSize);
var keysetQuery = context.Set<Entity>()
.Where(e => e.SeqId > lastSeqId)
.OrderBy(e => e.SeqId)
.Take(pageSize);
// Execute both and measure execution time, rows scanned (via SET STATISTICS IO on SQL Server)
One detail that would clarify the recommendation: is the intended ordering column guaranteed unique and immutable across writes? If yes, keyset pagination is viable. If not, offset pagination with a stable sort key or a hybrid approach is safer.
Use comments to ask for clarification. Post a solution as an answer.
35,775 reputation · 11 Sept 2025, 03:46 UTC
The QueryPossibleUnintendedOrderBy warning (RelationalEventId.QueryPossibleUnintendedOrderBy) was introduced in EF Core 6.0 and behaves identically through EF Core 9.0. It triggers during query compilation — once per unique query shape — not on every page execution. This means a paginated endpoint logging at Information level will emit the warning a single time per distinct OrderBy/Where combination, regardless of how many pages clients request. Suppressing it via ConfigureWarnings is safe only after verifying every paginated query includes an explicit OrderBy; the warning exists because missing ordering produces non-deterministic results across page boundaries, not because of performance.