PostgreSQL Mastery
PostgreSQL Mastery lessons / Module 2: Querying a table

SELECT: filtering, sorting and paging

Level 1: Foundations20 min

SELECT is evaluated in a logical order that differs from the order you write it. Understanding that order explains which aliases you can use where, and why LIMIT without ORDER BY returns arbitrary rows.

This lesson is interactive. Run every example on a real PostgreSQL 18 server in your browser, read EXPLAIN ANALYZE plans, and take the quiz.
Open the interactive lesson
You will learn to
  • Filter with comparison operators, IN, BETWEEN and LIKE / ILIKE
  • Sort deterministically and page with LIMIT, OFFSET and FETCH ... WITH TIES
  • Explain the logical evaluation order of a SELECT

conceptThe concept

The logical order is: FROM (and joins) → WHERE → GROUP BY → HAVING → SELECT list (expressions and aliases) → DISTINCT → ORDER BY → LIMIT / OFFSET. That is why a column alias defined in the SELECT list can be used in ORDER BY but not in WHERE.

Tables are unordered sets. Without ORDER BY, PostgreSQL returns rows in whatever order the plan produces them, which can change after an UPDATE, a VACUUM or a different plan. If the order matters, say so, and add a unique column as a tiebreaker.

LIKE is case-sensitive pattern matching with % (any string) and _ (one character); ILIKE ignores case. FETCH FIRST n ROWS WITH TIES also returns rows that tie with the last one on the ORDER BY key.

whyWhy it matters

Most "random" bugs in reports and APIs come from paging without a stable sort: a row appears on two pages, or never. OFFSET paging also gets slower with every page because the server still reads and discards the skipped rows; the performance level shows keyset pagination as the fix.

Use caseReal-world scenario

An admin screen lists gold and platinum customers in Germany, sorted by name, 10 per page. The query below is exactly that, and adding id to the ORDER BY makes the paging stable when two customers share a name.

Try itExamples

Run and edit these in the interactive lesson.

Filter, sort, limit
SELECT id, name, city, tier
FROM customers
WHERE country = 'DE' AND tier <> 'standard'
ORDER BY name, id
LIMIT 10;
IN, BETWEEN and ILIKE
SELECT sku, name, category, price
FROM products
WHERE category IN ('books', 'toys')
  AND price BETWEEN 20 AND 120
  AND name ILIKE '%guide%' OR name ILIKE '%puzzle%'
ORDER BY price DESC;
Look carefully at the results: AND binds tighter than OR, so the price and category filters only apply to the first ILIKE. The next example fixes it with parentheses.
The same query with explicit parentheses
SELECT sku, name, category, price
FROM products
WHERE category IN ('books', 'toys')
  AND price BETWEEN 20 AND 120
  AND (name ILIKE '%guide%' OR name ILIKE '%puzzle%')
ORDER BY price DESC;

An alias from the SELECT list works in ORDER BY. WITH TIES keeps going while the price equals the last returned row.

Expressions, aliases and ties
SELECT name, price, round(price * 0.9, 2) AS sale_price
FROM products
ORDER BY sale_price DESC
FETCH FIRST 5 ROWS WITH TIES;
DISTINCT
SELECT DISTINCT country, city
FROM customers
ORDER BY country, city;

Avoid / preferDo this, not that

Avoid
SELECT name, price FROM products
WHERE category = 'books' OR category = 'toys' AND price < 20;
Prefer
SELECT name, price FROM products
WHERE category IN ('books', 'toys') AND price < 20;

AND binds tighter than OR, so the first query returns every book at any price. IN (or explicit parentheses) says what you mean.

Avoid
SELECT id, created_at FROM orders LIMIT 10;
Prefer
SELECT id, created_at FROM orders
ORDER BY created_at DESC, id DESC
LIMIT 10;

Without ORDER BY the row order is whatever the plan produces and can change between runs. Add a unique tie-breaker so paging is stable.

PitfallsCommon mistakes

  • Using a SELECT alias in WHERE (WHERE sale_price > 10). WHERE runs before the SELECT list is computed; repeat the expression or use a subquery.
  • Mixing AND and OR without parentheses. AND has higher precedence, which silently changes which rows match.
  • Paging with LIMIT/OFFSET on a non-unique sort key, so rows shift between pages.

LimitsWhen not to use it

Avoid SELECT * in application code: it fetches columns you do not need (including large TOASTed ones), breaks when columns are added or reordered, and prevents index-only scans. Use it freely when exploring in psql.

SecuritySecurity notes

Never build a WHERE clause by concatenating user input into the SQL string. Use bind parameters (WHERE email = $1) from your driver; that one habit prevents SQL injection.

ChallengeTry it yourself

List the five most expensive active books under 100, breaking ties by name.

Show one solution
One solution
SELECT name, price
FROM products
WHERE category = 'books' AND active AND price < 100
ORDER BY price DESC, name
LIMIT 5;

InterviewQuestions you should be able to answer

In what order does PostgreSQL logically evaluate the clauses of a SELECT?
FROM and joins, WHERE, GROUP BY, HAVING, window functions, the SELECT list, DISTINCT, ORDER BY, then LIMIT/OFFSET. This explains why aliases are visible in ORDER BY but not in WHERE or HAVING, and why window functions cannot be used in WHERE. The planner may physically reorder work, but results must match this logical order.
Why is a query without ORDER BY not guaranteed to return rows in insertion order?
A table is an unordered heap. Rows come back in the order the chosen plan produces them: a sequential scan follows physical layout, which changes as UPDATEs write new tuple versions elsewhere and VACUUM frees space; an index scan follows index order; parallel plans and synchronized scans interleave. Only ORDER BY guarantees order.

QuizCheck your understanding

4 questions with instant feedback in the interactive lesson; a wrong answer goes into your review deck.

Go deeperOfficial documentation