devnotes.

Aggregate Functions

The basics

SELECT
  count(*)         AS total_students,
  count(email)     AS with_email,
  avg(cgpa)        AS avg_cgpa,
  max(cgpa)        AS best,
  min(cgpa)        AS worst,
  sum(credits)     AS total_credits,
  round(stddev(cgpa), 3) AS spread
FROM students;

Every aggregate except count(*) skips NULL. So avg(cgpa) divides by the number of non-null values, not by the row count.

Counting

count(*)                  -- every row
count(column)             -- rows where column IS NOT NULL
count(DISTINCT column)    -- distinct non-null values
count(*) FILTER (WHERE cgpa > 3.5)   -- conditional count, Postgres style

count(*) on a large table always scans in Postgres because MVCC makes visibility per-transaction. For an approximate number, read reltuples from pg_class.

Collecting values

SELECT department,
       string_agg(name_en, ', ' ORDER BY name_en) AS students,
       array_agg(id ORDER BY cgpa DESC)           AS ids_by_rank,
       jsonb_agg(jsonb_build_object('name', name_en, 'cgpa', cgpa)) AS json_rows
FROM students
GROUP BY department;

string_agg is the Postgres equivalent of MySQL's GROUP_CONCAT, but with no length limit to raise. jsonb_agg and jsonb_object_agg let you return a nested JSON document straight from SQL, which often removes an entire layer of application code.

FILTER

SELECT
  department,
  count(*)                              AS total,
  count(*) FILTER (WHERE cgpa >= 3.5)   AS excellent,
  avg(cgpa) FILTER (WHERE age < 21)     AS avg_young,
  round(100.0 * count(*) FILTER (WHERE cgpa >= 3.5) / count(*), 1) AS pct_excellent
FROM students
GROUP BY department;

Ordered-set aggregates

SELECT
  department,
  percentile_cont(0.5) WITHIN GROUP (ORDER BY cgpa) AS median_cgpa,
  percentile_disc(0.9) WITHIN GROUP (ORDER BY cgpa) AS p90,
  mode() WITHIN GROUP (ORDER BY grade)              AS most_common_grade
FROM students
GROUP BY department;

percentile_cont interpolates between values; percentile_disc returns an actual value from the data.

Aggregates and WHERE

WHERE runs before grouping, so aggregates are not allowed there:

-- ERROR
SELECT department FROM students WHERE avg(cgpa) > 3.0 GROUP BY department;

-- correct
SELECT department FROM students GROUP BY department HAVING avg(cgpa) > 3.0;

Custom aggregates

Postgres lets you define your own aggregate from a state function — rarely needed, but a reminder of how extensible the system is:

CREATE AGGREGATE product(numeric) (
  SFUNC = numeric_mul,
  STYPE = numeric,
  INITCOND = '1'
);
SELECT product(cgpa) FROM students;
PostgreSQL Aggregate Functions: count, sum, avg, array_agg | DevNotes