19/08/2026

WHERE and HAVING clauses in T-SQL

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

WHERE and HAVING clauses in T-SQL

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 gr...