Intermediate PostgreSQL

GROUP BY and HAVING in PostgreSQL

Aggregate data into groups and filter grouped results with HAVING clauses.

5 min read Tutorial

GROUP BY collapses rows that share common values into summary rows, which you then analyze with aggregate functions. HAVING filters those groups, much like WHERE filters individual rows.

Basic GROUP BY

-- Count orders per status
SELECT status, COUNT(*) AS order_count
FROM orders
GROUP BY status;

-- Revenue by product category
SELECT
  p.category,
  SUM(oi.quantity * oi.unit_price) AS revenue,
  COUNT(DISTINCT o.id) AS total_orders
FROM order_items oi
JOIN orders o ON oi.order_id = o.id
JOIN products p ON oi.product_id = p.id
GROUP BY p.category
ORDER BY revenue DESC;

Every column in the SELECT list must either be in the GROUP BY clause or wrapped in an aggregate function like COUNT, SUM, or AVG.

Filtering Groups with HAVING

-- Only categories with more than $1000 in revenue
SELECT
  p.category,
  SUM(oi.quantity * oi.unit_price) AS revenue
FROM order_items oi
JOIN products p ON oi.product_id = p.id
GROUP BY p.category
HAVING SUM(oi.quantity * oi.unit_price) > 1000;

-- Customers with 5 or more orders
SELECT
  customer_id,
  COUNT(*) AS order_count
FROM orders
GROUP BY customer_id
HAVING COUNT(*) >= 5;

WHERE runs before grouping and cannot reference aggregates. HAVING runs after grouping and is specifically designed to filter on aggregate values.

Grouping by Expressions

-- Orders per month
SELECT
  DATE_TRUNC('month', created_at) AS month,
  COUNT(*) AS orders,
  SUM(total) AS revenue
FROM orders
GROUP BY DATE_TRUNC('month', created_at)
ORDER BY month;

Common Use Cases

  • Sales reports grouped by product, region, or time period
  • Finding customers who exceed usage thresholds
  • Dashboard widgets showing counts by category or status
  • Detecting anomalies such as users with unusually high activity

Try this query in UnifySQL

Write, optimize, and collaborate on PostgreSQL queries with AI assistance.

Start Free