Skip to content

SQL · Query Basics

SQL ORDER BY, LIMIT & OFFSET — Sorting and Paging Results

5 min read Updated 2026-06-20 Share:

Practice Sorting & Limiting interview questions

Why sort and limit matter

A SELECT without ORDER BY returns rows in an undefined order — the database is free to return them however it pleases, and that order can change between runs as data grows or indexes are rebuilt. Any result set shown to a user, exported to a file, or compared in a test needs an explicit ORDER BY.

LIMIT (and its SQL-standard equivalent FETCH FIRST) caps how many rows come back. Together, ORDER BY + LIMIT are the building blocks of ranked lists, dashboards, and paginated APIs.

ORDER BY basics

-- Newest orders first
SELECT id, customer_id, total_amount, created_at
FROM orders
ORDER BY created_at DESC;

-- Alphabetically by last name, then first name
SELECT id, first_name, last_name
FROM customers
ORDER BY last_name ASC, first_name ASC;

-- Cheapest in-stock products first
SELECT product_name, unit_price, stock_quantity
FROM products
WHERE stock_quantity > 0
ORDER BY unit_price ASC;

ASC (ascending, A→Z, 0→9) is the default and can be omitted. Columns not in SELECT can still appear in ORDER BY — the sort happens before projection.

Multi-column sorts

Each column in ORDER BY gets its own direction. The database sorts by the first column, then breaks ties using the second, and so on.

-- High-value orders on top; within same total, most recent first
SELECT id, customer_id, total_amount, created_at
FROM orders
ORDER BY total_amount DESC, created_at DESC;

-- Product grid: category ascending, then price ascending within category
SELECT product_name, category, unit_price
FROM products
ORDER BY category ASC, unit_price ASC;

NULL ordering

NULL is neither greater than nor less than any value. Where NULLs sort depends on the database:

  • Postgres / Oracle: NULLs sort last in ASC, first in DESC.
  • MySQL / SQL Server: NULLs sort first in ASC.

Use explicit NULLS FIRST / NULLS LAST (Postgres, Oracle) or a CASE workaround (MySQL, SQL Server) to control this:

-- Postgres: tasks with no due date sink to the bottom
SELECT id, title, due_date
FROM tasks
ORDER BY due_date ASC NULLS LAST;

-- MySQL equivalent (CASE pushes NULLs to position 1 → last)
SELECT id, title, due_date
FROM tasks
ORDER BY CASE WHEN due_date IS NULL THEN 1 ELSE 0 END, due_date ASC;

LIMIT and OFFSET

LIMIT n returns at most n rows. OFFSET k skips the first k rows. Together they paginate a result set.

-- Top 10 best-selling products
SELECT product_id, SUM(quantity) AS units_sold
FROM order_items
GROUP BY product_id
ORDER BY units_sold DESC
LIMIT 10;

-- Page 3 of a customer list (page size = 20)
SELECT id, email, created_at
FROM customers
ORDER BY created_at DESC
LIMIT 20 OFFSET 40;  -- skip pages 1 and 2

SQL Server / Oracle syntax:

-- SQL Server
SELECT TOP 10 * FROM orders ORDER BY total_amount DESC;

-- SQL:2008 standard (Postgres, SQL Server 2012+, Oracle 12c+)
SELECT * FROM orders ORDER BY total_amount DESC
FETCH FIRST 10 ROWS ONLY;

-- With offset
OFFSET 40 ROWS FETCH NEXT 20 ROWS ONLY;

The OFFSET pagination problem

OFFSET is convenient but has two problems at scale:

  1. Performance: the database reads and discards the first k rows on every request. Page 500 of 20 items means discarding 9 980 rows.
  2. Drift: if a row is inserted or deleted between page requests, items shift — the user sees a duplicate or skips a row.

Keyset (cursor) pagination — the fix

Instead of skipping rows by count, remember the last seen value and filter from there. This is O(log n) with an index instead of O(n).

-- First page: 20 newest orders
SELECT id, created_at, customer_id, total_amount
FROM orders
ORDER BY created_at DESC, id DESC
LIMIT 20;

-- Next page: pass the last row's (created_at, id) as the cursor
SELECT id, created_at, customer_id, total_amount
FROM orders
WHERE (created_at, id) < ('2026-06-15 10:23:00', 5831)
ORDER BY created_at DESC, id DESC
LIMIT 20;

The id tiebreaker ensures stable ordering when two rows share the same created_at. The index on (created_at DESC, id DESC) makes this fast at any page depth.

ORDER BY with expressions and CASE

You can sort by an expression or computed value directly in ORDER BY:

-- Sort by absolute value of profit margin (closest to zero first)
SELECT product_name, (revenue - cost) AS profit
FROM products
ORDER BY ABS(revenue - cost) ASC;

-- Custom priority: 'urgent' tickets first, then by created_at
SELECT id, title, priority, created_at
FROM support_tickets
ORDER BY CASE priority
           WHEN 'urgent'   THEN 1
           WHEN 'high'     THEN 2
           WHEN 'medium'   THEN 3
           ELSE 4
         END,
         created_at ASC;

Recap

Always pair ORDER BY with LIMIT — an unsorted limit is meaningless. Use NULLS LAST (or the CASE workaround) when nullable sort columns would push NULLs to the wrong end. Replace OFFSET-based pagination with keyset pagination for any list that grows beyond a few hundred rows — it stays fast regardless of page depth and avoids row-drift bugs.

More ways to practice

The self-quiz is live. Get notified when mock interviews and new question packs drop.

or
Join our WhatsApp Channel