Sorting and paging: ORDER BY, TOP, OFFSET and DISTINCT
Sort results by one or more columns, take the first few rows, page through a long list and remove duplicates. And why a query without ORDER BY has no order at all.
ORDER BY
ORDER BY sorts by one or more columns, each ASC (smallest first, the default) or DESC (largest first). With several columns, the second sorts only rows that tie on the first:
SELECT Name, CategoryId, PriceFROM dbo.ProductsWHERE CategoryId IN (1, 2, 3)ORDER BY CategoryId, Price DESC;Name CategoryId Price ------------------- ---------- ----- Cordless drill 1 89.00 Claw hammer 1 18.50 Tape measure 8 m 1 9.75 Cement 25 kg 2 7.20 Sand 25 kg 2 4.10 Plywood sheet 18 mm 3 42.00 Timber 2x4 2.4 m 3 6.80
SELECT FirstName, City, JoinedOnFROM dbo.CustomersORDER BY City, JoinedOn DESC;FirstName City JoinedOn --------- --------- ---------- Ella NULL 2023-09-14 Dev Bradford 2023-08-23 Zoë Harrogate 2024-02-15 Grace Leeds 2024-01-08 Chloe Leeds 2023-06-07 Asha Leeds 2023-03-02 Farid York 2023-11-30 Ben York 2023-05-19
TOP, WITH TIES and OFFSET … FETCH
TOP (n) returns the first n rows of the sorted result. Two products cost 6.80, so “the three cheapest” is ambiguous. WITH TIES settles it by including every row that ties with the last one:
SELECT TOP (3) WITH TIES Name, PriceFROM dbo.ProductsORDER BY Price;Name Price ---------------- ----- Sand 25 kg 4.10 Wall plugs (100) 4.50 Timber 2x4 2.4 m 6.80 Safety goggles 6.80
OFFSET … FETCH. It goes after ORDER BY and needs it. Here is page 2, four products per page:
SELECT ProductId, Name, PriceFROM dbo.ProductsORDER BY Price, ProductIdOFFSET 4 ROWS FETCH NEXT 4 ROWS ONLY;ProductId Name Price
--------- ----------------- -----
4 Cement 25 kg 7.20
3 Tape measure 8 m 9.75
8 Wood screws (200) 11.90
11 Paint roller set 12.50DISTINCT
SELECT DISTINCT removes duplicate rows from the result. Rows count as duplicates only if every column in the SELECT list matches, and DISTINCT treats NULLs as equal to each other:
SELECT DISTINCT CityFROM dbo.CustomersORDER BY City;City --------- NULL Bradford Harrogate Leeds York
SELECT DISTINCT CategoryId, UnitFROM dbo.ProductsWHERE CategoryId <= 3ORDER BY CategoryId;CategoryId Unit
---------- -----
1 each
2 bag
3 each
3 sheetThe full logical order
FROM: the source rowsWHERE: filter rowsSELECT: work out columns and aliasesDISTINCT: remove duplicate rowsORDER BY: sortTOP/OFFSET … FETCH: keep only some of the sorted rows
TOP without ORDER BY is a gamble: it takes the first rows of an unsorted result, which can be any rows. The Grouping module adds GROUP BY and HAVING between WHERE and SELECT.
Name, Price and Stock.
Show a hint
Show the solution
SELECT TOP (3) Name, Price, StockFROM dbo.ProductsWHERE Stock > 0ORDER BY Price, Name;Name Price Stock ---------------- ----- ----- Sand 25 kg 4.10 220 Timber 2x4 2.4 m 6.80 500 Cement 25 kg 7.20 300
Remember
Only ORDER BY guarantees the order of a result; without it, any order is possible. Sort by several columns, each ASC or DESC; NULLs sort first in ascending order. TOP (n)takes the first n sorted rows;WITH TIESadds rows that tie with the last one.OFFSET … FETCHpages through results; sort by something unique so pages do not overlap.DISTINCTremoves rows that match in every selected column.Logical order: FROM → WHERE → SELECT → DISTINCT → ORDER BY → TOP / OFFSET.