HAVING: Filtering the Groups Themselves
Lora wants to know which screenings are selling well: show me the screenings with more than ten bookings. It sounds like a job for WHERE — it's a filter, after all. But look at what's being filtered. "More than ten bookings" isn't a fact about any single booking row; it's a fact about a group of them. This filter tests piles, not rows, and WHERE cannot do it.
HAVING is WHERE's twin for exactly this job: the same true-or-false thinking, applied to groups after they exist instead of rows before. And the real lesson of this page is bigger than one keyword — understanding when each filter runs reveals that a query has an order of operations, just as arithmetic does. Learn that order once and you can debug any query you'll ever meet.
Why Can't WHERE Do This?
The obvious attempt is WHERE COUNT(*) > 10, and the database rejects it outright. The refusal is about timing. WHERE runs early, testing one raw row at a time as rows stream in from the table — at that moment, a booking row knows its seat and its screening and nothing more. "More than ten bookings" is a fact about a completed pile, and when WHERE runs, the piles do not exist yet. GROUP BY hasn't happened. There is nothing to count.
Back to last page's till receipts, then — same analogy, one page more of work to do. WHERE is the first pass: throw out the invalid receipts before sorting anything, one receipt at a time. Then the sorting into piles happens. HAVING is the second pass: now that the piles exist and each has been counted, throw out the piles that are too small. Two filters, same true-or-false shape, opposite sides of the sorting step.
HAVING Tests the Piles
Here is Lora's question for real. Group the bookings by screening, count each group, and keep only the groups whose count beats ten:
SELECT screening_id, COUNT(*) AS bookings_count FROM bookings GROUP BY screening_id HAVING COUNT(*) > 10;
| screening_id | bookings_count |
|---|---|
| 1 | 21 |
| 3 | 12 |
| 4 | 17 |
All fifty-four bookings were dealt into four piles; then HAVING tested each pile's count and dropped screening 2, whose four bookings didn't clear the bar. The thinking is pure WHERE — one test, true or false, keep or discard — with the unit changed from a row to a group. That's the entire keyword. What remains is to see the whole machine it fits into.
The Pipeline, Drawn Once
Every clause you've learned in this chapter occupies a fixed position in one processing order. Written queries start with SELECT, but that's not how they run. The database works in this sequence: FROM gathers the table, WHERE filters rows, GROUP BY builds the piles, HAVING filters the piles, SELECT computes what to show, ORDER BY sorts the survivors, and LIMIT cuts. One honest flag before you memorize it: this is the logical order — the order the answer must make sense in. Real engines rearrange the physical work under the hood for speed (Chapter 8 peeks at how), but they guarantee the result is exactly as if this order ran.
The pipeline explains mysteries you haven't hit yet, so file it deliberately. Why does the database reject an alias from the SELECT list inside WHERE? Because WHERE runs before SELECT computes the alias — the name doesn't exist yet at filtering time. Why must "top five busiest screenings" put ORDER BY after HAVING? Because only the surviving groups should race. Every "why can't I…" in SQL debugging is answered by pointing at two stages and noting which comes first.
WHERE and HAVING Together
The two filters aren't rivals; real queries routinely use both, each at its own stage. Lora's month-end question: among July's bookings only, which screenings drew more than ten? WHERE trims the rows to July before grouping; HAVING trims the groups after counting:
SELECT screening_id, COUNT(*) AS july_bookings FROM bookings WHERE booked_at >= '2026-07-01' GROUP BY screening_id HAVING COUNT(*) > 10;
| screening_id | july_bookings |
|---|---|
| 1 | 18 |
| 4 | 15 |
Walk it through the pipeline in one breath: FROM brings all bookings; WHERE keeps only rows booked on or after July 1st; GROUP BY piles the July rows by screening; HAVING keeps the piles that count past ten; SELECT shows each surviving pile's id and count. Screening 3 made the earlier all-time list at twelve bookings, but only nine of them were July — so it clears the first filter row by row and falls at the second. That's the division of labor, and with it the read side of SQL is complete but for one gap: those ids should be film titles, and the titles live in another table. Chapter 4 connects them.
- "HAVING is fancy WHERE." Same test shape, different stage and different unit: WHERE tests rows before grouping, HAVING tests groups after. Using HAVING without a GROUP BY is almost always a sign the query wants WHERE.
- "
WHERE COUNT(*) > 10should work." COUNT doesn't exist row by row — aggregates are facts about piles, and when WHERE runs, the piles haven't been built. That timing gap is exactly why HAVING exists. - "The query runs top to bottom as written." It's written SELECT-first but runs FROM-first. The logical pipeline explains every "why can't I use my alias here?" mystery you'll ever hit.
- "WHERE and HAVING compete — pick one." They cooperate at different stages: WHERE trims rows so the piles are built from the right material; HAVING then judges the finished piles. Real queries often need both.
- The logical pipeline is the single most transferable model in SQL — reading, writing, and debugging queries all lean on knowing which stage runs when.
- Groups-versus-rows was the last conceptual piece of read-side SQL. After this page only JOINs remain, and you already know why you need them: the ids in your answers want to be titles.
Knowledge Check
Why does the database reject WHERE COUNT(*) > 10?
- COUNT results can't be compared with >
- WHERE runs before grouping, so there are no piles to count yet
- WHERE only works on text columns, not numbers
- A query may only ever contain a single condition per individual clause
What exactly does HAVING filter?
- Individual rows, before they're grouped
- Columns that fail the test
- Whole groups, after GROUP BY has built and counted them
- The final answer at the very end, cutting it down to a fixed size
A query has FROM, WHERE, GROUP BY, HAVING, SELECT, ORDER BY, and LIMIT. In the logical pipeline, which runs first and which last?
- SELECT first, LIMIT last
- FROM first, LIMIT last
- WHERE first, ORDER BY last
- FROM first, SELECT last
In the July query, screening 3 has 12 bookings all-time but only 9 in July. Why is it missing from the result?
- WHERE dropped all of its rows for failing the July date test
- GROUP BY excluded it from the piles
- Its July pile counted 9, and HAVING requires more than 10
- LIMIT cut it from the end of the list
You got correct