PostgreSQL Aggregates & GROUP BY

Aggregate functions

SQL
SELECT
    COUNT(*)      AS total_books,
    COUNT(author) AS with_author,   -- COUNT(col) skips NULLs
    SUM(price)    AS total_value,
    AVG(price)    AS average_price,
    MIN(price)    AS cheapest,
    MAX(price)    AS priciest
FROM books;

COUNT(*) counts rows; COUNT(column) counts non-NULL values. The difference matters more often than people expect.

GROUP BY

SQL
SELECT author, COUNT(*) AS book_count, AVG(price) AS avg_price
FROM books
GROUP BY author
ORDER BY book_count DESC;

Rule: every column in the SELECT must either appear in GROUP BY or be inside an aggregate function. Postgres enforces this strictly (some other databases don't, and let you get silently wrong answers).

SQL
-- error: "title" must appear in the GROUP BY clause
SELECT author, title, COUNT(*) FROM books GROUP BY author;

HAVING

WHERE filters rows before grouping; HAVING filters groups after:

SQL
SELECT author, COUNT(*) AS book_count
FROM books
WHERE price > 200            -- filter individual books first
GROUP BY author
HAVING COUNT(*) >= 3          -- then keep only prolific authors
ORDER BY book_count DESC;

Put a condition in WHERE whenever you can — it reduces the number of rows that need grouping.

Grouping by several columns

SQL
SELECT
    author,
    EXTRACT(YEAR FROM published) AS year,
    COUNT(*)
FROM books
GROUP BY author, EXTRACT(YEAR FROM published)
ORDER BY author, year;

FILTER — conditional aggregates

A Postgres feature that beats CASE WHEN for readability:

SQL
SELECT
    COUNT(*) AS total,
    COUNT(*) FILTER (WHERE price < 300)  AS cheap,
    COUNT(*) FILTER (WHERE price >= 300) AS expensive,
    AVG(price) FILTER (WHERE published >= '2020-01-01') AS recent_avg
FROM books;

string_agg and array_agg

Collapse a group into one value:

SQL
SELECT
    a.name,
    string_agg(b.title, ', ' ORDER BY b.title) AS titles,
    array_agg(b.id) AS book_ids
FROM authors a
JOIN books b ON b.author_id = a.id
GROUP BY a.name;

Rounding money

SQL
SELECT author, ROUND(AVG(price), 2) AS avg_price
FROM books
GROUP BY author;

AVG on a NUMERIC column returns a lot of decimal places — round it for display.