iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

GROUP BY / Aggregates

GROUP BY collapses rows that share a key into one. Pair it with aggregate functions (COUNT, SUM, AVG, STRING_AGG, JSON_AGG) and filter the groups with HAVING.

Aggregates, HAVING, GROUPING SETS, window vs group

EXAMPLE
-- 1) Basic — count per group
SELECT  country, COUNT(*) AS users
FROM    users
GROUP   BY country
ORDER   BY users DESC;

-- 2) Multiple aggregates
SELECT  category,
        COUNT(*)        AS n,
        AVG(price)::numeric(10,2) AS avg_price,
        MIN(price)      AS min_price,
        MAX(price)      AS max_price,
        SUM(stock)      AS total_stock
FROM    products
GROUP   BY category;

-- 3) HAVING — filter the GROUPS (not the rows)
SELECT  user_id, COUNT(*) AS posts
FROM    posts
GROUP   BY user_id
HAVING  COUNT(*) >= 10
ORDER   BY posts DESC;

-- 4) GROUP BY ROLLUP / CUBE / GROUPING SETS — subtotals + totals in one query
SELECT  country, plan, COUNT(*) AS n
FROM    users
GROUP   BY ROLLUP (country, plan);
-- → per (country, plan) + per (country) + grand total

-- 5) JSON / array aggregates — pull related rows along
SELECT  u.id, u.email,
        JSON_AGG(p ORDER BY p.created_at DESC) AS posts,
        STRING_AGG(t.name, ', ' ORDER BY t.name) FILTER (WHERE t.id IS NOT NULL) AS tags
FROM    users u
LEFT    JOIN posts p ON p.user_id = u.id
LEFT    JOIN user_tags ut ON ut.user_id = u.id
LEFT    JOIN tags t ON t.id = ut.tag_id
GROUP   BY u.id, u.email;

-- 6) Filtered aggregates — FILTER clause (PostgreSQL extension)
SELECT
    COUNT(*)                                  AS total,
    COUNT(*) FILTER (WHERE status = 'paid')   AS paid,
    SUM(total) FILTER (WHERE status = 'paid') AS revenue,
    AVG(total) FILTER (WHERE status = 'paid') AS avg_paid
FROM    orders
WHERE   created_at >= now() - INTERVAL '30 days';

-- 7) GROUP BY vs WINDOW functions
--    GROUP BY collapses rows. Windows attach a per-row aggregate.
SELECT  user_id, post_id,
        ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at) AS n,
        COUNT(*)   OVER (PARTITION BY user_id) AS total_posts
FROM    posts;

Why it matters

FILTER (WHERE …) is Postgres’ underused superpower for “conditional aggregate” queries. One pass, no CASE WHEN ladders, perfectly index-able.

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
SELECT date_trunc('day', created_at) AS day, count(*) AS n
FROM events
WHERE created_at >= now() - interval '7 days'
GROUP BY 1
ORDER BY 1;
Try it Yourself »

Discussion

Loading…