Without notes, state yesterday’s main idea and one unresolved question.
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.ANDbinds tighter thanOR, 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:
LIKEwith%meaning "any run of characters" and_meaning "any one character".ILIKEis the same but case-insensitive, which is what user search boxes want. - Emptiness:
IS NULLandIS 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.
- Load
db/seed.sqlso your data matches this lesson. - Write a query listing every piece of equipment with status
maintenance. Verify: 1 row. - Write one searching serial numbers for a fragment, using
ILIKEand%. Verify it finds the right rows and misses the others. - Write one listing the five most recent maintenance records, newest first, using
ORDER BYandLIMIT. - Write one combining two conditions with
AND, over a date comparison and a status. - Write one using
INwith three status values, then a sixth usingBETWEENonperformed_on. - For one query, deliberately swap
IS NULLfor= NULLand record what comes back. Then fix it. - Save each query with its comment. Run the whole file with
psql maintenance -f db/queries.sqland 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"givesERROR: column "Chiller 1" does not exist, because double quotes mean an identifier. Text takes single quotes. WHEREafterORDER BY. The clause order is fixed. Out of order, you getsyntax 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 intoWHERE. - Trusting
LIMITwithoutORDER 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.
- The tutorial shows
DISTINCT, which this lesson skipped. Work out what it does and write a query using it overequipment.status. How many rows come back, and why? - 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
- 0–5 min Recall
- 5–20 min Learn
Read only the listed concept notes and official reference sections needed today.
- 20–48 min Build
Query equipment by status, search serial numbers, sort recent records, and limit results.
- 48–55 min Explain and verify
Run the result, inspect evidence, and explain the data/control flow in your own words.
- 55–60 min Quiz and commit
Complete the quiz, record one lesson, and commit the verified change when applicable.
What to hand in
A queries.sql file with at least six verified queries.
Working with AI today
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
Explain-back gate
Pass the quiz above to unlock completion.
Quiz + explain-back checks required.