NHibernate QueryOver API ↔ SQL Server Pagination: ensuring deterministic paging with .OrderBy()
0 reputation · 18 Mar 2020, 20:54 UTC
0 reputation · 18 Mar 2020, 20:54 UTC
When implementing server‑side paging with NHibernate’s QueryOver, the typical pattern is .OrderBy() followed by .Skip(n) and .Take(m). The goal is to retrieve page n of a stable result set without duplicates or gaps. The constraints are that the underlying SQL dialect may translate these calls into different constructs (LIMIT/OFFSET, TOP/FETCH, or ROW_NUMBER), and that a missing or unstable .OrderBy() can lead to non‑deterministic results. The uncertainty lies in whether NHibernate enforces a deterministic order when the same .OrderBy() expression is used across different database back‑ends, and how large .Skip() values affect performance and correctness on each dialect.
Specific questions:
.OrderBy() expression yields a stable ordering across all supported SQL dialects?.Skip() values on PostgreSQL or Oracle?.OrderBy() with .Skip() and .Take() to avoid duplicate or missing rows in paging scenarios?34775 reputation · 19 Mar 2020, 08:17 UTC
To ensure stable pagination in NHibernate using .OrderBy(), .Skip(), and .Take(), you must order by a unique set of columns. If the sorting criteria are not unique, the database engine is not obligated to maintain the same row order across different page requests, which leads to duplicate or missing records.
NHibernate does not guarantee that a given .OrderBy() expression yields identical stability across all SQL dialects. NHibernate acts as a translator; it converts the QueryOver API into dialect-specific SQL. For example:
OFFSET n ROWS FETCH NEXT m ROWS ONLY.ROW_NUMBER() window functions.LIMIT m OFFSET n.While the intent of the order is preserved, the underlying database engine's handling of non-unique sort keys varies. If multiple rows have the same value in the OrderBy column, the database may return them in any order, regardless of the dialect.
Across most relational databases, including PostgreSQL and Oracle, large .Skip() values result in linear performance degradation. This is because the database must typically scan and discard n rows before it can return the requested m rows. As the offset increases, the I/O and CPU cost to reach that offset increases proportionally.
To prevent duplicates or gaps, follow these implementation rules:
CreatedDate or Status) alone. Always append a unique identifier (usually the Primary Key) as the final sort criteria.// Example of deterministic paging
var results = session.QueryOver<Order>()
.OrderBy(x => x.OrderDate).Asc
.OrderBy(x => x.Id).Asc // Unique tie-breaker ensures stability
.Skip(pageIndex * pageSize)
.Take(pageSize)
.List();
To verify your pagination is deterministic, perform the following:
log4net output to confirm if the dialect is using OFFSET/FETCH or ROW_NUMBER().Diagnostic Requirement: To provide a more specific performance recommendation, please specify the version of SQL Server (or other DB) and the NHibernate version currently in use, as the translation strategy for .Skip() changed significantly between older and newer dialects.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.