Course outline

PostgreSQL / Basic: relational foundations / Lesson 5

Aggregates and reporting stages

Produce reliable counts, totals and conditional summaries. Separate pre-group filtering from group filtering and distinguish an empty set from a zero measure.

About 4 min readContent reviewed
On this page

Lesson goals

  • Explain COUNT, SUM, GROUP BY, HAVING and FILTER.
  • Validate group grain, NULL behavior and deterministic output.

Grouping changes what a row means

Before grouping, a row may represent one payment. After GROUP BY customer_id, it represents one customer summary. Every projected value must follow a valid grouping or aggregate rule; do not rely on permissive behavior copied from another database. WHERE filters input rows before aggregation, while HAVING selects completed groups. A report of customers whose total exceeds fifty differs from a report that first removes every payment of fifty or less. Write that distinction in plain language before SQL.

Observe how NULL changes aggregate inputs

The fixture contains two known amounts for customer 1 and one NULL amount for customer 2. COUNT(*) counts rows, COUNT(amount) counts non-NULL amounts and SUM ignores NULL inputs but returns NULL when no non-NULL input exists. The output therefore distinguishes one unknown payment record from no payment rows. COALESCE can display zero when that is the report policy, but applying it prematurely can hide missing financial facts that should instead trigger investigation.

sql Illustrative SQL; expected results are reasoned, not measured.
SELECT customer_id, COUNT(*) AS records,
 COUNT(amount) AS known_amounts, SUM(amount) AS total
FROM (VALUES (1, 10::numeric), (1, 15::numeric),
             (2, NULL::numeric)) AS p(customer_id, amount)
GROUP BY customer_id ORDER BY customer_id;
Expected output
customer_id | records | known_amounts | total
1           | 2       | 2             | 25
2           | 1       | 0             | NULL

The NULL total is a deliberate unknown result, not a fabricated zero.

FILTER documents conditional aggregates

PostgreSQL supports FILTER on aggregate calls. It lets one report count paid and pending records without duplicating source joins or moving the entire report population into one condition. COUNT(*) FILTER counts qualifying rows, while SUM(amount) FILTER can still be NULL when no qualifying known amounts exist. Conditions obey three-valued logic: unknown is not true. Decide whether cancelled bookings, refunds and unknown statuses belong to a metric before presenting it as a meaningful operational number.

Compute two metrics from one fixture

This self-contained relation has two paid rows and one pending row. The conditional counts are two and one, while the paid amount is thirty. A WHERE state = paid would eliminate pending input before its count could be calculated. FILTER leaves the shared population visible to each aggregate and changes only that aggregateโ€™s input. The example uses a stable literal fixture; it makes no claim about real payment throughput or observed production totals.

sql Illustrative SQL; expected results are reasoned, not measured.
SELECT COUNT(*) FILTER (WHERE state = 'paid') AS paid_count,
 COUNT(*) FILTER (WHERE state = 'pending') AS pending_count,
 SUM(amount) FILTER (WHERE state = 'paid') AS paid_total
FROM (VALUES ('paid', 10), ('paid', 20), ('pending', 5))
 AS p(state, amount);
Expected output
paid_count | pending_count | paid_total
2          | 1             | 30

All three metrics are derived from the same three input records.

Ordering and arithmetic require review

GROUP BY does not guarantee result order; add ORDER BY for exports and stable comparisons. Integer division truncates in PostgreSQL when both operands are integer types, so cast an operand to numeric for ratios. AVG ignores NULL values, which changes its denominator relative to COUNT(*). Reconcile a sample report by hand at its documented grain, including empty inputs and repeated amounts. A query that executes successfully can still answer the wrong question when the population or denominator is poorly defined.

Common errors and fixes

column must appear in GROUP BY (42803)

Why it happens
A projected value has no valid grouping or aggregate rule.
How to fix it
Group the intended dimensions or aggregate with a justified rule.
Prevent it next time
State the report grain before choosing output columns.

A ratio unexpectedly becomes zero

Why it happens
Both operands used integer division.
How to fix it
Cast one operand to numeric and define zero-denominator handling.
Prevent it next time
Test fractions and empty populations in report calculations.

An outer-join count shows one for an empty child set

Why it happens
COUNT(*) counted the preserved parent row.
How to fix it
Count the non-NULL child key when counting children.
Prevent it next time
Include a parent with no children in aggregate fixtures.

Check your understanding

1. The report has amounts 10, NULL and 20. Compare COUNT(*), COUNT(amount), SUM and AVG.

Show worked solution

COUNT(*) is 3, COUNT(amount) is 2, SUM is 30 and AVG is 15 over the two known amounts. Replacing NULL with zero before AVG would instead yield 10 and assert a different business meaning. Decide whether the unknown record is incomplete data, an intentional absence or a zero-value event before choosing a denominator.

2. A success ratio uses successes / attempts, both integers. Show a safe expression and explain a zero attempt count.

Show worked solution

Use successes::numeric / NULLIF(attempts, 0). The cast preserves a fractional ratio and NULLIF prevents division by zero. A zero denominator produces NULL, meaning the rate is undefined rather than a measured zero success rate. Presentation can label it unavailable; avoid reporting 0% as though failed attempts had actually occurred.

sql Worked solution for the stated synthetic assumptions.
SELECT 1::numeric / NULLIF(4, 0) AS ratio;
Expected output
ratio
0.25

Key takeaways

  • NULL-aware denominators and explicit grain make totals interpretable.
  • FILTER and HAVING operate at different reporting stages.

Official references