SQL Syllabus

Week 1 ยท SQL Basics / Day 3

Day 3: ORDER BY, TOP, OFFSET-FETCH, functions

Thu 1 October 2026 ยท 33 queries ยท run on AdventureWorks2022

Goal: control the order and size of results, and reshape values with string and date functions.

Read: SQL Order By, SQL Select Top, SQL Aliases, and the SQL Server Functions reference (string and date sections).

On this page

  1. Drill 18
  2. Stretch: string functions 3
  3. Stretch: date functions 2
  4. Stretch: other functions 4
  5. Self-test 6

Drill

#1

1. Ascending, descending, multi-column

SELECT Name, ListPrice FROM Production.Product ORDER BY ListPrice;

What this query does

ORDER BY sorts the result. The default direction is ascending (ASC), smallest first, so the 0-priced components come first. ORDER BY is the only way to guarantee the order of rows; without it SQL Server returns rows in whatever order is quickest.

Result in SSMS: 504 rows
#2

1. Ascending, descending, multi-column (continued)

SELECT Name, ListPrice FROM Production.Product ORDER BY ListPrice DESC;

What this query does

DESC reverses the order: most expensive first. The Road-150 bikes at 3578.27 are at the top.

Result in SSMS: 504 rows
#3

1. Ascending, descending, multi-column (continued)

SELECT Name, Color, ListPrice FROM Production.Product ORDER BY Color, ListPrice DESC;

What this query does

Sorting by two columns: first by Color (A to Z), then, inside each colour, by ListPrice from highest to lowest. Each column has its own direction: DESC only applies to ListPrice, Color is still ascending.

Result in SSMS: 504 rows
#4

2. Three-level sort

SELECT FirstName, LastName, PersonType FROM Person.Person
ORDER BY PersonType, LastName, FirstName;

What this query does

Three sort levels. SQL Server sorts by PersonType first; ties are broken by LastName; any remaining ties are broken by FirstName. Like a phone book grouped by type.

Result in SSMS: 19,972 rows
#5

3. Top 5 dearest, top 5 cheapest that are actually sold

SELECT TOP 5 Name, ListPrice FROM Production.Product ORDER BY ListPrice DESC;

What this query does

TOP 5 with ORDER BY ... DESC is the standard 'top N' pattern: sort from most expensive and keep the first five.

Result in SSMS: 5 rows
#6

3. Top 5 dearest, top 5 cheapest that are actually sold (continued)

SELECT TOP 5 Name, ListPrice FROM Production.Product WHERE ListPrice > 0 ORDER BY ListPrice;

What this query does

Without WHERE ListPrice > 0 the cheapest five would all be components priced 0. The WHERE runs first and removes them, then ORDER BY sorts the remaining products, then TOP keeps five.

WHERE ListPrice > 0 removes the components, otherwise the cheapest five would all be 0.

Result in SSMS: 5 rows
#7

4. Order by a column not selected. Works. Add DISTINCT and it fails.

SELECT Name FROM Production.Product ORDER BY ListPrice DESC;

What this query does

You can sort by a column you are not showing. SQL Server still has ListPrice available for each row while it sorts.

Result in SSMS: 504 rows
#8

4. Order by a column not selected. Works. Add DISTINCT and it fails. (continued)

SELECT DISTINCT Name FROM Production.Product ORDER BY ListPrice DESC;

What this query does

Error 145. DISTINCT first collapses the rows down to unique names. After that, one name could in theory stand for several rows with different prices, so SQL Server does not know which ListPrice to sort by. Rule: with DISTINCT, every ORDER BY column must be in the SELECT list.

Error 145: with DISTINCT, ORDER BY items must appear in the select list.

Result in SSMS: Error (expected): ORDER BY items must appear in the select list if SELECT DISTINCT is specified.
#9

5. Paging. Page 2 then page 3, 10 per page.

SELECT ProductID, Name FROM Production.Product
ORDER BY ProductID OFFSET 10 ROWS FETCH NEXT 10 ROWS ONLY;

What this query does

OFFSET 10 ROWS skips the first 10 rows (page 1); FETCH NEXT 10 ROWS ONLY returns the following 10 (page 2). OFFSET-FETCH must come after an ORDER BY, because pages only make sense in a fixed order.

Result in SSMS: 10 rows
#10

5. Paging. Page 2 then page 3, 10 per page. (continued)

SELECT ProductID, Name FROM Production.Product
ORDER BY ProductID OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY;

What this query does

Page 3 skips 20 rows. The general formula for page n with page size s is OFFSET (n - 1) * s ROWS FETCH NEXT s ROWS ONLY. This is how websites show results one page at a time.

Page n of size s: OFFSET (n - 1) * s ROWS FETCH NEXT s ROWS ONLY.

Result in SSMS: 10 rows
#11

6. WITH TIES. Compare counts.

SELECT TOP 3 Name, ListPrice FROM Production.Product ORDER BY ListPrice DESC;

What this query does

TOP 3 returns exactly 3 rows. But five Road-150 bikes share the top price of 3578.27, so two of them are cut off, and which two is not defined.

Result in SSMS: 3 rows
#12

6. WITH TIES. Compare counts. (continued)

SELECT TOP 3 WITH TIES Name, ListPrice FROM Production.Product ORDER BY ListPrice DESC;

What this query does

WITH TIES also returns any row whose ORDER BY value equals the last row's value. Because five bikes cost 3578.27, this returns 5 rows instead of 3. WITH TIES requires an ORDER BY.

WITH TIES also returns every row that ties with the last one, so it returns 5 rows, not 3.

Result in SSMS: 5 rows
#13

7. Aliases with and without AS, table alias

SELECT p.Name AS ProductName, p.ListPrice Price FROM Production.Product AS p;

What this query does

Aliases rename things. Column aliases (ProductName, Price) change the heading in the result. AS is optional: 'p.ListPrice Price' works too, but AS is clearer. The table alias p is a short name for Production.Product, so columns can be written p.Name. Table aliases become essential when joining several tables.

Result in SSMS: 504 rows
#14

7. Aliases with and without AS, table alias (continued)

SELECT p.Name, p.ListPrice FROM Production.Product p;

What this query does

The same table alias without AS. When a table has an alias you refer to its columns through the alias (p.Name).

Result in SSMS: 504 rows
#15

8. Order by position. Works. Bad habit.

SELECT Name, ListPrice FROM Production.Product ORDER BY 2 DESC;

What this query does

ORDER BY 2 means 'sort by the second column in the SELECT list' (ListPrice). It works, but it is a bad habit: if someone later adds or reorders columns, the query silently sorts by the wrong column with no error. Always write the column name.

Bad habit: if the SELECT list changes, ORDER BY 2 silently sorts by a different column.

Result in SSMS: 504 rows
#16

9. NULLs sort first ascending in SQL Server. Push them last.

SELECT Name, Color FROM Production.Product ORDER BY Color;

What this query does

In SQL Server, NULL counts as lower than any value, so in an ascending sort the 248 products with no colour come first.

Result in SSMS: 504 rows
#17

9. NULLs sort first ascending in SQL Server. Push them last. (continued)

SELECT Name, Color FROM Production.Product
ORDER BY CASE WHEN Color IS NULL THEN 1 ELSE 0 END, Color;

What this query does

The CASE expression makes a hidden sort key: 1 for NULL colours, 0 for everything else. Sorting on that first puts real colours (0) before NULLs (1); the second sort key, Color, then sorts the colours alphabetically. SQL Server has no NULLS LAST keyword, so this is the standard workaround.

The CASE gives NULL colours a 1 and the rest a 0, so NULLs sort last.

Result in SSMS: 504 rows
#18

10. The latest order. Memorise this shape.

SELECT TOP 1 SalesOrderID, OrderDate, TotalDue FROM Sales.SalesOrderHeader
ORDER BY OrderDate DESC;

What this query does

TOP 1 + ORDER BY ... DESC returns the single most recent order. This shape (latest login, newest order, highest score) comes up constantly. If several orders share the latest date, add a second sort column such as SalesOrderID DESC so the result is predictable.

Result in SSMS: 1 row

Stretch: string functions

#19

String functions

SELECT TOP 10
    FirstName,
    UPPER(FirstName) AS Upper,
    LOWER(LastName) AS Lower,
    LEN(LastName) AS LastNameLength,
    LEFT(FirstName, 1) AS Initial,
    CONCAT(FirstName, ' ', MiddleName, ' ', LastName) AS FullName,   -- CONCAT treats NULL as empty; + does not
    REPLACE(LastName, 'a', '@') AS Replaced,
    REVERSE(FirstName) AS Backwards
FROM Person.Person;

What this query does

UPPER and LOWER change case. LEN counts the characters (ignoring trailing spaces). LEFT(text, n) takes the first n characters, here the initial. CONCAT joins values and treats NULL as an empty string, so FullName is never NULL, unlike the + operator on Day 1. REPLACE swaps every 'a' for '@' (case-insensitive here because of the CI collation). REVERSE writes the text backwards.

CONCAT treats NULL as an empty string, so FullName is never NULL (compare Day 1 drill 9).

Result in SSMS: 10 rows
#20

Extract the part before @ in emails

SELECT TOP 10 EmailAddress, LEFT(EmailAddress, CHARINDEX('@', EmailAddress) - 1) AS Username
FROM Person.EmailAddress;

What this query does

CHARINDEX('@', EmailAddress) returns the position of the @ (for example 6). LEFT(..., position - 1) keeps everything before it. The -1 removes the @ itself. A common pattern for splitting text at a separator.

Result in SSMS: 10 rows
#21

Format a price with thousands separators

SELECT TOP 5 Name, FORMAT(ListPrice, 'N2') AS Price FROM Production.Product WHERE ListPrice > 0;

What this query does

FORMAT(value, 'N2') turns a number into text with thousands separators and 2 decimal places (3,578.27). The result is text, so use FORMAT only for display, not for further calculations. FORMAT is also slow on large tables.

Result in SSMS: 5 rows

Stretch: date functions

#22

Date functions

SELECT TOP 10
    SalesOrderID,
    OrderDate,
    YEAR(OrderDate) AS Yr,
    MONTH(OrderDate) AS Mth,
    DATENAME(month, OrderDate) AS MonthName,
    DATENAME(weekday, OrderDate) AS DayName,
    DATEDIFF(day, OrderDate, ShipDate) AS DaysToShip,
    DATEADD(day, 30, OrderDate) AS PaymentDue,
    EOMONTH(OrderDate) AS MonthEnd,
    CAST(OrderDate AS DATE) AS DateOnly,
    CONVERT(varchar, OrderDate, 103) AS UKFormat,
    FORMAT(OrderDate, 'yyyy-MM') AS YearMonth
FROM Sales.SalesOrderHeader;

What this query does

YEAR and MONTH return numbers. DATENAME returns names ('May', 'Tuesday'). DATEDIFF(day, start, end) counts the days between two dates. DATEADD(day, 30, date) adds 30 days. EOMONTH gives the last day of the month. CAST(... AS DATE) drops the time. CONVERT(varchar, date, 103) formats as dd/mm/yyyy (style 103 = British). FORMAT(date, 'yyyy-MM') gives a year-month label, useful for monthly reports.

Result in SSMS: 10 rows
#23

Current date and time

SELECT GETDATE(), SYSDATETIME(), GETUTCDATE();

What this query does

GETDATE() returns the current local date and time (datetime, about 3 ms precision). SYSDATETIME() returns the same moment with higher precision (datetime2, 100 ns). GETUTCDATE() returns the current time in UTC. Pakistan time is UTC+5, so GETUTCDATE() is 5 hours behind GETDATE() on this PC.

Result in SSMS: 1 row

Stretch: other functions

#24

ISNULL and COALESCE

SELECT TOP 10 Name, Color,
    ISNULL(Color, 'No colour') AS ColorOrDefault,
    COALESCE(Color, Class, 'Unknown') AS FirstNonNull
FROM Production.Product;

What this query does

ISNULL(Color, 'No colour') replaces a NULL with a default value. COALESCE takes any number of arguments and returns the first one that is not NULL: the colour if there is one, otherwise the class, otherwise 'Unknown'. COALESCE is standard SQL and works in other databases; ISNULL is SQL Server only.

Result in SSMS: 10 rows
#25

Maths functions

SELECT TOP 5 Name, ListPrice,
    ROUND(ListPrice * 1.17, 2) AS WithTax,
    CEILING(ListPrice) AS Up,
    FLOOR(ListPrice) AS Down,
    ListPrice % 100 AS Remainder
FROM Production.Product WHERE ListPrice > 0;

What this query does

ROUND(x, 2) rounds to 2 decimal places. CEILING always rounds up to the next whole number, FLOOR always rounds down. % is modulo: the remainder after dividing by 100 (for 337.22 it is 37.22).

Result in SSMS: 5 rows
#26

CAST to change data type

SELECT TOP 5 CAST(ListPrice AS INT) AS Whole,
    CAST(ListPrice AS VARCHAR(20)) + ' USD' AS Labelled
FROM Production.Product WHERE ListPrice > 0;

What this query does

CAST(value AS type) converts between data types. CAST(ListPrice AS INT) removes the decimals (SQL Server rounds money values to the nearest whole number here). Casting to VARCHAR turns the number into text so it can be joined to ' USD' with +; adding a number and text directly would cause a conversion error.

Result in SSMS: 5 rows
#27

Random row

SELECT TOP 1 Name FROM Production.Product ORDER BY NEWID();   -- random row

What this query does

NEWID() creates a new random unique identifier for every row. Sorting by it shuffles the rows, and TOP 1 picks the first, so you get a random product each time you run it. Fine for small tables; slow on very large ones because every row must be sorted.

Result in SSMS: 1 row

Self-test

#28

Q1. The 3 newest orders showing ID and date only

SELECT TOP 3 SalesOrderID, OrderDate FROM Sales.SalesOrderHeader ORDER BY OrderDate DESC;

What this query does

The 'latest' shape from drill 10, with TOP 3 and only the two requested columns.

Result in SSMS: 3 rows
#29

Q2. Products page 4 of 25 per page, by name

SELECT ProductID, Name FROM Production.Product
ORDER BY Name OFFSET 75 ROWS FETCH NEXT 25 ROWS ONLY;

What this query does

Page 4 with 25 per page skips (4 - 1) * 25 = 75 rows and fetches the next 25. Rows 76 to 100 in name order.

Result in SSMS: 25 rows
#30

Q3. Every person as LASTNAME, Firstname

SELECT UPPER(LastName) + ', ' + UPPER(LEFT(FirstName, 1))
       + LOWER(SUBSTRING(FirstName, 2, LEN(FirstName))) AS PersonName
FROM Person.Person;

What this query does

UPPER(LastName) gives the capitals. For 'Firstname' casing, the first letter is made upper case with UPPER(LEFT(FirstName, 1)) and the rest lower case with LOWER(SUBSTRING(FirstName, 2, LEN(FirstName))). SUBSTRING(text, start, length) cuts out part of a string. The pieces are joined with +; neither name can be NULL in this table, so + is safe.

Result in SSMS: 19,972 rows
#31

Q4. Days between each order and its ship date, longest first

SELECT SalesOrderID, OrderDate, ShipDate, DATEDIFF(day, OrderDate, ShipDate) AS DaysToShip
FROM Sales.SalesOrderHeader
ORDER BY DaysToShip DESC;

What this query does

DATEDIFF(day, OrderDate, ShipDate) calculates the gap in days. ORDER BY can use the alias DaysToShip because ORDER BY runs after SELECT. (WHERE runs before SELECT, so it could not use the alias.) AdventureWorks orders ship 7 or 8 days after ordering, so the 8-day orders come first.

Result in SSMS: 31,465 rows
#32

Q5. The second most expensive product without OFFSET

SELECT TOP 1 Name, ListPrice
FROM (SELECT TOP 2 Name, ListPrice FROM Production.Product ORDER BY ListPrice DESC) AS t
ORDER BY ListPrice;

What this query does

The inner query (subquery) takes the 2 most expensive products. The outer query sorts those two ascending and keeps the first, which is the second most expensive. Watch out for ties: five products share the top price, so this returns another 3578.27 product. The next query solves the 'second highest price' version.

Ties: the two dearest products cost the same, so this returns a 3578.27 product. The next query asks for the second highest price.

Result in SSMS: 1 row
#33

Q5. The second most expensive product without OFFSET (continued)

SELECT TOP 1 ListPrice
FROM (SELECT DISTINCT TOP 2 ListPrice FROM Production.Product ORDER BY ListPrice DESC) AS t
ORDER BY ListPrice;

What this query does

Adding DISTINCT inside the subquery takes the two highest different prices (3578.27 and 3399.99). The outer query keeps the lower of those, so the answer is the second highest price, 3399.99. Subqueries are covered properly on Day 6.

Result in SSMS: 1 row