Skip to content
EL
Contents

Aggregation and grouping

HAVING, FILTER and conditional aggregation

ConceptFoundation2 min read10 min hands-on#fundamentals#sql

The idea in 30 seconds

WHERE filters rows before grouping; HAVING filters groups after. To count or sum only some of the rows in each group, add FILTER (WHERE …) to the aggregate. With FILTER you can turn one pass over a table into a whole summary: totals, subtotals and breakdowns side by side.

Helps to know first: Joins and relational querying

On this page

HAVING filters groups

Customers with at least two orders:

SELECT customer_id, count(*) AS orders
FROM orders
GROUP BY customer_id
HAVING count(*) >= 2
ORDER BY customer_id;
 customer_id | orders
-------------+--------
           1 |      3
           2 |      2
           3 |      2
           4 |      2
           6 |      2

The condition is about the group, so it goes in HAVING. Putting it in WHERE is an error:

SELECT customer_id, count(*) FROM orders WHERE count(*) >= 2 GROUP BY customer_id;
ERROR:  aggregate functions are not allowed in WHERE

WHERE runs before the groups exist, so there is no count to compare yet.

WHEREHAVING
RunsBefore groupingAfter grouping
FiltersRowsGroups
Can use aggregatesNoYes

Use both together when you need both: WHERE status <> 'cancelled' decides which orders are counted, and HAVING count(*) >= 2 decides which customers are shown.

FILTER: aggregate only some rows

Often you want several counts from the same group: all orders, shipped orders, open orders. Add FILTER (WHERE …) to an aggregate and it only sees the rows that pass:

SELECT customer_id,
       count(*)                                           AS orders,
       count(*) FILTER (WHERE status = 'shipped')         AS shipped,
       count(*) FILTER (WHERE status IN ('pending', 'paid')) AS open
FROM orders
GROUP BY customer_id
ORDER BY customer_id;
 customer_id | orders | shipped | open
-------------+--------+---------+------
           1 |      3 |       2 |    1
           2 |      2 |       0 |    2
           3 |      2 |       1 |    0
           4 |      2 |       1 |    1
           6 |      2 |       1 |    1
           7 |      1 |       1 |    0

One pass over the table, one row per customer, three numbers. This is called conditional aggregation, and it is how you turn rows into a side-by-side summary (sometimes called a pivot).

You will also see it written with CASE, which works in databases without FILTER:

sum(CASE WHEN status = 'shipped' THEN 1 ELSE 0 END) AS shipped

Check yourself

  1. When does a condition belong in HAVING rather than WHERE?

    Show answer

    When it tests an aggregate or the group as a whole, such as count(*) >= 2. WHERE runs before grouping and cannot use aggregates.

  2. Write the aggregate that counts only cancelled orders in each group.

    Show answer

    count(*) FILTER (WHERE status = 'cancelled'), or sum(CASE WHEN status = 'cancelled' THEN 1 ELSE 0 END).

  3. A query has WHERE status <> 'cancelled' and also count(*) FILTER (WHERE status = 'cancelled'). What does the filtered count return?

    Show answer

    0 for every group. WHERE already removed the cancelled rows before grouping, so the filtered aggregate never sees any.

Finished reading?

Saved in this browser only.

Sources

Where each claim on this page comes from.

  1. 1.
    PostgreSQL documentation: 2.7 Aggregate Functions (tutorial)
    Primary· §2.7 Aggregate Functions

    WHERE selects input rows before groups and aggregates are computed; HAVING selects group rows after; WHERE must not contain aggregate functions.

  2. 2.
    PostgreSQL documentation: 4.2 Value Expressions
    Primary· §4.2.7 Aggregate Expressions

    With FILTER, only input rows for which the filter clause is true are fed to the aggregate.

  3. 3.
    PostgreSQL documentation: 9.18 Conditional Expressions
    Primary· §9.18.1 CASE

    CASE chooses a value by condition.

Go further

What to read next, and why it is worth the time.

  1. 1.
    PostgreSQL documentation: 2.7 Aggregate Functions (tutorial)
    Primary· §2.7

    The tutorial's explanation of WHERE versus HAVING is the clearest one available.

Applies to PostgreSQL 18. Draft: not yet fact-checked end to end.