0 / 91
Week 7 · Day 45 of 91

SELECT, WHERE, ORDER BY, and LIMIT

SQL and PostgreSQL

Objective

Retrieve precise data instead of loading everything.

A relational schema resembles a disciplined wiring and labeling plan: constraints prevent invalid connections and keys define relationships.

  • projection and filtering
  • sorting
  • limiting results

Why this matters

Yesterday you put rows into a database. Today you get precise answers back out. A query that returns exactly the three columns and five rows you need is the difference between an API that stays fast at a million rows and one that loads the entire table into memory to find one pump.

By the end of the hour you can name the four clauses that shape almost every read in a real application — which columns, which rows, in what order, how many — and you will have written and verified six queries of your own.

Getting everyone to the same data

So that your output matches this lesson's, start from a known set of rows. Create db/seed.sql:

TRUNCATE maintenance_records, equipment RESTART IDENTITY CASCADE;

INSERT INTO equipment (name, serial_number, status, installed_on) VALUES
  ('Feed Pump 3',      'PMP-0003', 'operational', '2021-06-14'),
  ('Chiller 1',        'CHL-0001', 'operational', '2020-03-02'),
  ('Air Compressor A', 'CMP-0011', 'maintenance', '2019-11-20'),
  ('Feed Pump 4',      'PMP-0004', 'operational', '2022-08-05'),
  ('Conveyor B',       'CNV-0002', 'retired',     '2015-01-30');

INSERT INTO maintenance_records (equipment_id, performed_on, hours_spent, notes) VALUES
  (1, '2024-02-11', 1.50, 'Replaced mechanical seal'),
  (1, '2024-09-03', 0.75, 'Checked shaft alignment'),
  (2, '2024-05-19', 3.25, 'Coil clean and refrigerant top-up'),
  (3, '2025-01-08', 4.00, 'Rebuilt compressor valve plate'),
  (1, '2025-03-22', 2.00, 'Bearing replacement');
psql maintenance -f db/seed.sql

`TRUNCATE` empties tables instantly

TRUNCATE deletes every row in the named tables — no WHERE, no undo, no confirmation. RESTART IDENTITY also resets the id counter to 1, and CASCADE extends the emptying to tables whose foreign keys point here. It is right for a disposable practice database you are resetting on purpose, and wrong everywhere else. Never run it against data you cannot recreate from a file.

Projection: choosing columns

A SELECT names the columns you want. That choice is called projection.

SELECT name, status FROM equipment;
       name       |   status
------------------+-------------
 Feed Pump 3      | operational
 Chiller 1        | operational
 Air Compressor A | maintenance
 Feed Pump 4      | operational
 Conveyor B       | retired
(5 rows)

SELECT * means "every column". It is fine for exploring at the psql prompt and a poor habit in application code: it moves data you do not need over the network, and it silently changes shape the day someone adds a column. Name your columns.

You can rename a column in the output with AS, which matters later when two joined tables both have an id:

SELECT name AS equipment_name, installed_on AS commissioned FROM equipment;

Filtering: choosing rows

WHERE is the clause that filters rows. It takes a condition, and only rows for which the condition is true come back.

SELECT name, serial_number FROM equipment WHERE status = 'operational';
    name     | serial_number
-------------+---------------
 Feed Pump 3 | PMP-0003
 Chiller 1   | CHL-0001
 Feed Pump 4 | PMP-0004
(3 rows)

Note = for comparison. SQL is not JavaScript: a single = compares, and <> (or !=) means "not equal". Text values go in single quotes.

The conditions you will use constantly:

  • Comparison: =, <>, <, >, <=, >=. They work on dates too: installed_on < '2021-01-01'.
  • Combining: AND, OR, NOT. AND binds tighter than OR, so parenthesise when you mix them.
  • Set membership: status IN ('operational', 'maintenance').
  • Range: performed_on BETWEEN '2024-01-01' AND '2024-12-31' — inclusive at both ends.
  • Pattern match: LIKE with % meaning "any run of characters" and _ meaning "any one character". ILIKE is the same but case-insensitive, which is what user search boxes want.
  • Emptiness: IS NULL and IS NOT NULL.
SELECT name, installed_on FROM equipment
WHERE status <> 'retired' AND installed_on < '2021-01-01';
       name       | installed_on
------------------+--------------
 Chiller 1        | 2020-03-02
 Air Compressor A | 2019-11-20
(2 rows)

`= NULL` is never true

NULL means "unknown", and comparing anything to an unknown yields unknown — not true. So WHERE notes = NULL returns zero rows even when notes are missing, and it does so silently, with no error. Use WHERE notes IS NULL. This is one of the few SQL rules with no analogy that makes it feel natural; memorise it.

`WHERE` is a filter on a signal path

A SELECT is a probe on the whole bus. WHERE is a band-pass filter placed before your instrument: the unwanted rows never reach it. The important consequence is where the work happens — filtering in the database means the rows are discarded at the source, while fetching everything and filtering in JavaScript means every row crosses the wire first. Same answer, wildly different cost.

Sorting and limiting

ORDER BY sorts the result. ASC (ascending) is the default; DESC reverses it. LIMIT caps how many rows come back, and OFFSET skips rows first — together they are how the pagination you built in Week 6 is actually implemented.

SELECT id, equipment_id, performed_on, notes
FROM maintenance_records
ORDER BY performed_on DESC
LIMIT 3;
 id | equipment_id | performed_on |             notes
----+--------------+--------------+--------------------------------
  5 |            1 | 2025-03-22   | Bearing replacement
  4 |            3 | 2025-01-08   | Rebuilt compressor valve plate
  2 |            1 | 2024-09-03   | Checked shaft alignment
(3 rows)

The clauses are written in a fixed order — SELECT, FROM, WHERE, ORDER BY, LIMIT — and they are also applied roughly in that order: rows are found, filtered, sorted, and only then cut down. That is why LIMIT 3 gives you the three newest records rather than three arbitrary ones that were then sorted.

The picking list

You do not send a warehouse worker to bring back the entire aisle so you can look through it at the desk. You send a list: these items, in this order, at most this many. WHERE, ORDER BY, and LIMIT are that list, and the walk to the aisle happens once.

`LIMIT` without `ORDER BY` is not repeatable

Without ORDER BY, a table's row order is undefined, and PostgreSQL may return them differently after an update or a plan change. LIMIT 10 alone means "any ten rows", not "the first ten". Every LIMIT in application code needs an ORDER BY beside it.

Walkthrough: from a question to a query

Requests arrive in English. Translate one clause at a time.

"Show the two most recently installed pumps that are not retired, newest first."

Start with the table and the columns — projection:

SELECT name, serial_number, installed_on FROM equipment;

Add the row filter. "Pumps" means the serial number starts with PMP-, and "not retired" is a second condition:

SELECT name, serial_number, installed_on FROM equipment
WHERE serial_number LIKE 'PMP-%' AND status <> 'retired';
    name     | serial_number | installed_on
-------------+---------------+--------------
 Feed Pump 3 | PMP-0003      | 2021-06-14
 Feed Pump 4 | PMP-0004      | 2022-08-05
(2 rows)

Then order and limit:

SELECT name, serial_number, installed_on FROM equipment
WHERE serial_number LIKE 'PMP-%' AND status <> 'retired'
ORDER BY installed_on DESC
LIMIT 2;
    name     | serial_number | installed_on
-------------+---------------+--------------
 Feed Pump 4 | PMP-0004      | 2022-08-05
 Feed Pump 3 | PMP-0003      | 2021-06-14
(2 rows)

Building it in stages is not slower — it is how you tell which clause broke when the answer looks wrong.

Checkpoint

Predict the output of SELECT name FROM equipment WHERE name ILIKE '%pump%'; before running it. (Two rows: Feed Pump 3 and Feed Pump 4. ILIKE ignores case, so lowercase pump still matches Pump.)

Your turn

Build db/queries.sql with at least six queries, each preceded by a comment stating the question in English and the row count you got.

  1. Load db/seed.sql so your data matches this lesson.
  2. Write a query listing every piece of equipment with status maintenance. Verify: 1 row.
  3. Write one searching serial numbers for a fragment, using ILIKE and %. Verify it finds the right rows and misses the others.
  4. Write one listing the five most recent maintenance records, newest first, using ORDER BY and LIMIT.
  5. Write one combining two conditions with AND, over a date comparison and a status.
  6. Write one using IN with three status values, then a sixth using BETWEEN on performed_on.
  7. For one query, deliberately swap IS NULL for = NULL and record what comes back. Then fix it.
  8. Save each query with its comment. Run the whole file with psql maintenance -f db/queries.sql and confirm every query still returns what your comment claims.

Pair mode — while you write steps 2 to 6

"Give me query requirements one at a time. Let me write SQL before showing corrections." Write your attempt first, run it, then compare. Before accepting any AI-generated change: inspect the diff line by line, run your checks, and understand the behaviour it produces. A query you cannot explain is one you cannot debug at 400,000 rows.

You are done when

db/queries.sql runs top to bottom with no errors and every comment's claimed row count matches what psql prints.

Common pitfalls

  • Double quotes around text. WHERE name = "Chiller 1" gives ERROR: column "Chiller 1" does not exist, because double quotes mean an identifier. Text takes single quotes.
  • WHERE after ORDER BY. The clause order is fixed. Out of order, you get syntax error at or near "WHERE".
  • Filtering in JavaScript instead of SQL. Fetching all rows and using .filter() works on 5 rows and collapses on 500,000. Push the condition into WHERE.
  • Trusting LIMIT without ORDER BY. It returns some rows, not the first rows.

Verify it yourself

Open today's reference, the PostgreSQL tutorial, and read its section on querying a table.

  1. The tutorial shows DISTINCT, which this lesson skipped. Work out what it does and write a query using it over equipment.status. How many rows come back, and why?
  2. This lesson claimed row order is undefined without ORDER BY. Find the documentation's own wording on that and record whether it agrees.

Add both to db/queries.sql as comments. Tomorrow you start changing rows, where a missing WHERE costs considerably more than a wrong answer.

The hour

  1. 0–5 min Recall

    Without notes, state yesterday’s main idea and one unresolved question.

  2. 5–20 min Learn

    Read only the listed concept notes and official reference sections needed today.

  3. 20–48 min Build

    Query equipment by status, search serial numbers, sort recent records, and limit results.

  4. 48–55 min Explain and verify

    Run the result, inspect evidence, and explain the data/control flow in your own words.

  5. 55–60 min Quiz and commit

    Complete the quiz, record one lesson, and commit the verified change when applicable.

What to hand in

Deliverable

A queries.sql file with at least six verified queries.

Working with AI today

AI as pair programmer

Define one small task, review the plan, inspect the diff, run checks, and explain every changed section.

Give me query requirements one at a time. Let me write SQL before showing corrections.

References

End-of-day quiz

Q1 Which clause filters rows?
Q2 Which result best proves today’s work is complete?
Q3 Before accepting an AI-generated code change, what should you do?

Explain-back gate

Pass the quiz above to unlock completion.

Quiz + explain-back checks required.