The essentials
Quick reference
One focused task per row. Jump to the related section for complete, working examples.
| Use | Syntax | Examples |
|---|---|---|
| Count rows | SELECT COUNT(*) FROM orders; | View examples |
| Count non-null values | SELECT COUNT(shipped_at) FROM orders; | View examples |
| Count distinct values | SELECT COUNT(DISTINCT customer_id) FROM orders; | View examples |
| Sum and average values | SELECT SUM(amount), AVG(amount) FROM payments; | View examples |
| Find range endpoints | SELECT MIN(created_at), MAX(created_at) FROM events; | View examples |
| Group equal values | SELECT status, COUNT(*) FROM orders GROUP BY status; | View examples |
| Group by several expressions | SELECT region, status, SUM(amount)
FROM orders
GROUP BY region, status; | View examples |
| Filter detail rows | SELECT team, SUM(score)
FROM results
WHERE season = 2026
GROUP BY team; | View examples |
| Filter summary groups | SELECT team, SUM(score)
FROM results
GROUP BY team
HAVING SUM(score) >= 100; | View examples |
| Filter one aggregate | COUNT(*) FILTER (WHERE status = 'paid') AS paid_count | View examples |
| Sum selected values | SUM(amount) FILTER (WHERE status = 'paid') AS paid_total | View examples |
| Join text in order | STRING_AGG(name, ', ' ORDER BY name) AS names | View examples |
| Collect values in order | ARRAY_AGG(id ORDER BY created_at, id) AS ids | View examples |
| Require every condition | BOOL_AND(approved) AS all_approved | View examples |
| Choose several summaries | GROUP BY GROUPING SETS ((region, product), (region), ()) | View examples |
| Build hierarchical subtotals | GROUP BY ROLLUP (region, product) | View examples |
| Build all subtotal combinations | GROUP BY CUBE (region, channel) | View examples |
| Identify subtotal columns | GROUPING(region, product) AS grouping_mask | View examples |
| Default an empty total | COALESCE(SUM(amount), 0) AS total | View examples |
Aggregation turns sets of rows into summary values. Filter detail rows before grouping, select only grouped expressions or aggregates, order aggregates whose result depends on order, and use grouping indicators when subtotal rows can otherwise be confused with real null values.
Step by step
Detailed examples
Choose the aggregate that matches the question
COUNT(*) counts rows, while COUNT(expression) ignores null results and COUNT(DISTINCT expression) also removes duplicates. Most aggregates ignore null inputs; SUM, AVG, MIN, and MAX return null rather than zero for no input rows. Numeric return types vary with the input type, so check precision requirements for financial and statistical work.
WITH orders (customer_id, shipped_at, amount) AS (
VALUES (1, DATE '2026-01-01', 20), (1, NULL, 30), (2, DATE '2026-01-02', 50)
)
SELECT COUNT(*) AS rows,
COUNT(shipped_at) AS shipped,
COUNT(DISTINCT customer_id) AS customers,
SUM(amount) AS total,
AVG(amount) AS average
FROM orders; rows | shipped | customers | total | average
-----+---------+-----------+-------+---------
3 | 2 | 2 | 100 | 33.3333Return one row per grouping key
GROUP BY combines rows with equal grouped expressions, then aggregates operate within each group. Every selected expression must be grouped, aggregated, or functionally dependent in a way PostgreSQL recognizes. Grouping does not guarantee output order, so add a final ORDER BY when presentation must be deterministic.
WITH orders (region, status, amount) AS (
VALUES ('East', 'paid', 40), ('East', 'paid', 20),
('East', 'open', 15), ('West', 'paid', 30)
)
SELECT region, status, COUNT(*) AS orders, SUM(amount) AS total
FROM orders
GROUP BY region, status
ORDER BY region, status; region | status | orders | total
-------+--------+--------+------
East | open | 1 | 15
East | paid | 2 | 60
West | paid | 1 | 30Use WHERE for rows and HAVING for groups
WHERE runs before grouping and is the efficient place for predicates on detail rows. HAVING runs after aggregation and can reference aggregate expressions to reject whole groups. Moving a condition between them changes the data being summarized, so express the business question before optimizing the query.
WITH results (season, team, score) AS (
VALUES (2026, 'A', 60), (2026, 'A', 50), (2026, 'B', 70), (2025, 'B', 80)
)
SELECT team, SUM(score) AS total
FROM results
WHERE season = 2026
GROUP BY team
HAVING SUM(score) >= 100
ORDER BY team; team | total
-----+------
A | 110Calculate several conditional summaries in one grouping pass
FILTER attaches a WHERE-like predicate to one aggregate. This is clearer than repeating CASE expressions and allows paid, open, failed, or other measures to share the same grouped rows. A filtered SUM returns null when nothing qualifies, so apply COALESCE only when zero is the correct domain value.
WITH orders (region, status, amount) AS (
VALUES ('East', 'paid', 40), ('East', 'open', 15), ('East', 'paid', 20)
)
SELECT region,
COUNT(*) AS all_orders,
COUNT(*) FILTER (WHERE status = 'paid') AS paid_orders,
SUM(amount) FILTER (WHERE status = 'paid') AS paid_total,
COUNT(*) FILTER (WHERE status = 'open') AS open_orders
FROM orders
GROUP BY region; region | all_orders | paid_orders | paid_total | open_orders
-------+------------+-------------+------------+------------
East | 3 | 2 | 60 | 1Order inputs inside order-sensitive aggregates
STRING_AGG, ARRAY_AGG, JSON aggregates, and similar collectors can produce meaningfully different results for different input orders. Put ORDER BY inside the aggregate call so it controls that aggregate's input rather than only sorting final rows. BOOL_AND and BOOL_OR summarize predicates but ignore nulls, which should be handled explicitly when unknown is significant.
WITH members (team, id, name, joined_at, approved) AS (
VALUES ('Docs', 2, 'Grace', DATE '2026-02-01', true),
('Docs', 1, 'Ada', DATE '2026-01-01', true)
)
SELECT team,
STRING_AGG(name, ', ' ORDER BY name) AS names,
ARRAY_AGG(id ORDER BY joined_at, id) AS ids,
BOOL_AND(approved) AS all_approved
FROM members
GROUP BY team; team | names | ids | all_approved
-----+------------+-------+-------------
Docs | Ada, Grace | {1,2} | trueProduce explicit subtotal levels
GROUPING SETS lists exactly the summaries required. ROLLUP expands prefixes for hierarchical totals, while CUBE expands every subset and can grow output quickly. Subtotal rows use null placeholders for omitted grouping expressions; GROUPING returns a mask that distinguishes those placeholders from real null group values.
WITH sales (region, product, amount) AS (
VALUES ('East', 'A', 10), ('East', 'B', 20), ('West', 'A', 15)
)
SELECT region, product, SUM(amount) AS total,
GROUPING(region, product) AS grouping_mask
FROM sales
GROUP BY ROLLUP (region, product)
ORDER BY GROUPING(region, product), region, product; region | product | total | grouping_mask
-------+---------+-------+--------------
East | A | 10 | 0
East | B | 20 | 0
West | A | 15 | 0
East | NULL | 30 | 1
West | NULL | 15 | 1
NULL | NULL | 45 | 3Define empty-set and null semantics intentionally
Except for COUNT, built-in aggregates commonly return null when no rows are aggregated. COALESCE that result only when the domain defines an empty total as zero or an empty collection. On outer joins, COUNT(*) counts the preserved row even without a match; count a non-null column from the optional side when the goal is related-row count.
WITH teams (id) AS (VALUES (1), (2)),
scores (team_id, points) AS (VALUES (1, 10), (1, 20))
SELECT t.id,
COUNT(s.team_id) AS score_rows,
COALESCE(SUM(s.points), 0) AS points
FROM teams AS t
LEFT JOIN scores AS s ON s.team_id = t.id
GROUP BY t.id
ORDER BY t.id; id | score_rows | points
---+------------+-------
1 | 2 | 30
2 | 0 | 0Sources and further reading
References
Authoritative documentation used to verify and expand this cheat sheet.
- PostgreSQL Global Development GroupPostgreSQL: Aggregate Functionspostgresql.org
- PostgreSQL Global Development GroupPostgreSQL: Aggregate Expressionspostgresql.org
- PostgreSQL Global Development GroupPostgreSQL: GROUP BY and HAVINGpostgresql.org
- PostgreSQL Global Development GroupPostgreSQL: GROUPING SETS, CUBE, and ROLLUPpostgresql.org
Help us improve
Found a typo or missing example?
Tell us what would make this cheat sheet clearer, more complete, or more useful.



