WHERE vs HAVING in SQL: Filter Rows and Groups Correctly
WHERE filters individual rows before grouping. HAVING filters groups after aggregation. Use both when a report needs a row-level condition and a condition on the resulting totals.
For example, “regions with at least 150 in paid sales” contains two different rules: only paid transactions contribute, and only regions whose paid total reaches 150 are shown. Putting both conditions in the same place changes the meaning or creates an invalid query.
| Condition | Clause |
|---|---|
| Keep paid transactions | WHERE status = ‘paid’ |
| Keep groups with a total of at least 150 | HAVING SUM(amount) >= 150 |
| Sort the final groups | ORDER BY |
Think through the logical stages: FROM, WHERE, GROUP BY, HAVING, SELECT, ORDER BY. This is a reasoning aid, not a promise about the optimizer’s physical execution order.
Try it yourself in SQLite
Open SQL Lab, paste this setup, and run it once. Then run each query separately. Table and column names stay in English so the same SQL works in every language. The lab interface and linked lessons are currently in English.
CREATE TABLE blog_sales (
id INTEGER PRIMARY KEY, region TEXT, status TEXT, amount INTEGER
);
INSERT INTO blog_sales VALUES
(1, 'North', 'paid', 100), (2, 'North', 'paid', 80),
(3, 'North', 'refunded', 500), (4, 'South', 'paid', 70),
(5, 'South', 'paid', 90), (6, 'West', 'paid', 40);
1. Apply row and group filters together
North has paid sales of 180, not 680: its refunded transaction must not enter the total. South totals 160 and West totals 40. WHERE removes the refunded transaction before grouping; HAVING then removes West.
SELECT region, SUM(amount) AS paid_total, COUNT(*) AS paid_orders
FROM blog_sales
WHERE status = 'paid'
GROUP BY region
HAVING SUM(amount) >= 150
ORDER BY region;
Expected result
| region | paid_total | paid_orders |
|---|---|---|
| North | 180 | 2 |
| South | 160 | 2 |
2. A row threshold removes transactions
Changing the row filter to amount >= 80 removes the paid transaction of 70 in South. Its resulting total becomes 90. This asks for totals of large individual transactions, not regions with large overall totals.
SELECT region, SUM(amount) AS total
FROM blog_sales
WHERE status = 'paid' AND amount >= 80
GROUP BY region
ORDER BY region;
Expected result
| region | total |
|---|---|
| North | 180 |
| South | 90 |
3. A group threshold keeps contributing rows
With the amount condition in HAVING instead, both paid South transactions contribute and its total stays 160. West is excluded because its group total is below 80. Similar-looking conditions can produce different business answers.
SELECT region, SUM(amount) AS total
FROM blog_sales
WHERE status = 'paid'
GROUP BY region
HAVING SUM(amount) >= 80
ORDER BY region;
Expected result
| region | total |
|---|---|
| North | 180 |
| South | 160 |
Practice and check your answer
Return regions with at least two paid transactions. Keep WHERE status = 'paid', group by region, and use HAVING COUNT(*) >= 2. The answer is North and South, each with two transactions.
A useful debugging sequence is to inspect the rows surviving WHERE, run the grouped query without HAVING, and then add the group filter. This exposes whether missing data comes from a row filter or an aggregate threshold.
Do not put SUM(amount) in WHERE for this grouped query; the group total is not available at that stage. Also avoid selecting an ungrouped, nonaggregated column such as id: strict engines reject it, and permissive engines may return a value that does not represent the group.
Use the aggregate expression in HAVING rather than relying on its SELECT alias if you want more portable SQL. Do not use HAVING as a replacement for every WHERE filter. A condition on each input row belongs in WHERE unless you intentionally want different group semantics.
Documentation and next steps
Continue with the GROUP BY and HAVING lesson. Examples run in SQLite. The row/group distinction applies across common relational databases, but alias rules and permissive GROUP BY behavior vary by engine.
Try the examples in SQL Lab
Copy the setup and queries from this guide into the lab. The examples use SQLite. Lessons and lab instructions are currently in English.
Open SQL Lab