Sorting and pagination are fundamental to any API that returns lists. SQL Server’s OFFSET/FETCH NEXT syntax implements server-side pagination — only the requested page of rows is returned, not the entire table. This is what EF Core’s .Skip().Take() generates under the hood. Correct pagination requires a deterministic ORDER BY — without it, the same query can return different rows on different executions because SQL Server can return rows in any order.
ORDER BY and OFFSET/FETCH Pagination
-- ── Basic ORDER BY ─────────────────────────────────────────────────────────
SELECT Id, Title, ViewCount, PublishedAt
FROM dbo.Posts
WHERE IsPublished = 1
ORDER BY PublishedAt DESC; -- most recent first
-- ── Multi-column sort ──────────────────────────────────────────────────────
SELECT Id, Title, Status, ViewCount
FROM dbo.Posts
ORDER BY Status ASC, -- first sort by status
ViewCount DESC; -- then by views (descending) within each status
-- ── NULL ordering (NULLs sort first in ASC, last in DESC by default) ──────
-- Posts with NULL PublishedAt appear FIRST with ASC:
SELECT Id, Title, PublishedAt FROM dbo.Posts ORDER BY PublishedAt ASC;
-- To put NULLs last:
ORDER BY CASE WHEN PublishedAt IS NULL THEN 1 ELSE 0 END, PublishedAt ASC;
-- ── OFFSET/FETCH NEXT — server-side pagination ────────────────────────────
-- Parameters: @Page (1-based), @PageSize
DECLARE @Page INT = 2;
DECLARE @PageSize INT = 10;
SELECT
p.Id,
p.Title,
p.Slug,
p.ViewCount,
p.PublishedAt,
u.DisplayName AS AuthorName,
COUNT(*) OVER () AS TotalCount -- total rows WITHOUT paging (window function)
FROM dbo.Posts p
JOIN dbo.Users u ON u.Id = p.AuthorId
WHERE p.IsPublished = 1
ORDER BY p.PublishedAt DESC -- ← ORDER BY required for OFFSET/FETCH
OFFSET (@Page - 1) * @PageSize ROWS -- skip rows from previous pages
FETCH NEXT @PageSize ROWS ONLY; -- return only this page
-- For page 1: OFFSET 0 ROWS FETCH NEXT 10 ROWS ONLY → rows 1-10
-- For page 2: OFFSET 10 ROWS FETCH NEXT 10 ROWS ONLY → rows 11-20
-- For page 3: OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY → rows 21-30
-- ── Separate total count query (alternative to COUNT(*) OVER()) ───────────
-- More readable; two separate queries instead of one with window function
SELECT COUNT(*) AS TotalCount
FROM dbo.Posts
WHERE IsPublished = 1;
-- ── TOP WITH TIES — return top N including ties ────────────────────────────
SELECT TOP 3 WITH TIES Id, Title, ViewCount
FROM dbo.Posts
WHERE IsPublished = 1
ORDER BY ViewCount DESC;
-- If 3rd and 4th posts both have 500 views, all 4 are returned
COUNT(*) OVER() is a window function that computes the total count of qualifying rows and adds it as a column to every row in the result set — in a single query, without a separate COUNT query. This is how Entity Framework Core’s .CountAsync() before .Skip().Take() can be replaced with a single query. The window function is evaluated after the WHERE filter but before OFFSET/FETCH, giving the total count of matching rows regardless of the current page size.ORDER BY column. ORDER BY PublishedAt DESC works for most cases, but if two posts have the exact same PublishedAt timestamp, SQL Server may return them in different orders on different pages, causing rows to appear twice or be skipped entirely. Add ORDER BY PublishedAt DESC, Id DESC (a tiebreaker using the unique primary key) to guarantee consistent pagination across all pages.ORDER BY column_position (e.g., ORDER BY 1, 3) in production queries — it breaks when columns are added or reordered. Always use explicit column names or aliases: ORDER BY PublishedAt DESC, Title ASC. Column position ordering is only acceptable in ad-hoc exploratory queries where readability is not critical. It also cannot be used with OFFSET/FETCH in some SQL Server configurations.Common Mistakes
Mistake 1 — Non-deterministic ORDER BY for pagination (duplicate or missing rows across pages)
❌ Wrong — ORDER BY PublishedAt DESC alone; two posts with same timestamp may swap between pages.
✅ Correct — ORDER BY PublishedAt DESC, Id DESC — the unique Id as tiebreaker ensures deterministic order.
Mistake 2 — OFFSET/FETCH without ORDER BY (syntax error)
❌ Wrong — FETCH NEXT 10 ROWS ONLY without ORDER BY; SQL Server requires ORDER BY for OFFSET/FETCH.
✅ Correct — always include an ORDER BY clause before OFFSET.