Recent query I have worked on got me wondering what is the different between WHERE clause and HAVING clause in a T-SQL statement which do group by.
In simplest terms:
WHERE -> row level filter (eliminate rows not qualified for)
HAVING -> group filter (eliminate entire groups)
It get clear when you understand the logical query processing order in SQL Server.
FROM -> WHERE -> GROUP BY -> HAVING -> SELECT -> ORDER BY
As shown above, WHERE come before the group by, which mean query processor is eliminating rows as soon after it fetch rows from the store.
Remaining rows are then grouped.
Grouped and summarized (aggregated) rows then flow into HAVING, where they are get further filtered.
Finally, we select columns and order them.
Above is not to get confused with execution plan, this is not the order plan is physically executed.
Let us consider a example:
CREATE TABLE dbo.Orders
(
OrderId INT IDENTITY PRIMARY KEY,
CustomerId INT NOT NULL,
OrderDate DATE NOT NULL,
Status VARCHAR(20) NOT NULL,
Amount DECIMAL(10,2) NOT NULL
);
INSERT INTO dbo.Orders (CustomerId, OrderDate, Status, Amount)
VALUES
(1, '2026-01-10', 'Completed', 500.00),
(1, '2026-02-14', 'Completed', 300.00),
(1, '2026-03-02', 'Cancelled', 900.00),
(2, '2026-01-20', 'Completed', 150.00),
(2, '2026-02-25', 'Cancelled', 500.00),
(3, '2026-01-05', 'Completed', 750.00),
(3, '2026-03-11', 'Completed', 400.00);
Our query is following:
SELECT CustomerId,
SUM(Amount) AS TotalSpend,
COUNT(*) AS OrderCount
FROM dbo.Orders
WHERE Status = 'Completed' -- row-level filter, runs first
GROUP BY CustomerId
HAVING SUM(Amount) > 600; -- group-level filter, runs after aggregation
In our query, WHERE clause is: Status = 'Completed'
This filter out 2 rows from the initial fetch which belongs to customer 1 and 2.
Rest goes to Group by clause. After grouping we are left with following:
| Customer Id | TotalSpend | OrderCount |
|---|---|---|
| 1 | 800 | 2 |
| 2 | 150 | 1 |
| 3 | 1150 | 2 |
Above data set will flow into HAVING clause. HAVING clause has following condition:
SUM(Amount) > 600 (i.e. TotalSpend > 600)
Which means customer two's group will be completely eliminated in HAVING phase.
If we had just HAVING not WHERE clause, group two will appear in the final result set as it get TotalSpend of 650.
Also worth noting that COUNT(*) only count filtered rows from WHERE clause (see OrderCount number in above table).
HAVING without GROUP BY works
Another related thing to note is HAVING works without GROUP BY. In this case whole data set flowed from WHERE clause is considered as one group.
SELECT SUM(Amount) AS TotalSpend,
COUNT(*) AS OrderCount
FROM dbo.Orders
WHERE Status = 'Completed' -- row-level filter, runs first
HAVING SUM(Amount) > 600; -- group-level filter, runs after aggregation
This returns only one row (one group), with OrderCount = 5, because WHERE clause has eliminated cancelled orders.
Field Alias Usage
Field alias cannot be used in HAVING or even in WHERE clause. That is because HAVING and WHERE clause comes before SELECT. But you can use alias fields in ORDER BY.
See the query processing order given in the start of the this blog and you will understand why.
Final Word
If the condition is about a single row, it belongs in WHERE. If it's about the group as a whole, it belongs in HAVING. And filtering early in WHERE is generally cheaper too, since there's less data to sort and aggregate.
Hope you learn something, even it is simple. Having strong knowledge in simple stuff helps you to grab advanced stuff.
No comments:
Post a Comment