Basics of selection I · Topic 10 Free
Grouping: GROUP BY, aggregates, HAVING
Data model — the tables you will query
How the tables join
| Relationship | Cardinality | Join condition |
|---|---|---|
| students.cohort_id → cohorts.id | many students → one cohort | JOIN cohorts ON students.cohort_id = cohorts.id |
| enrollments.student_id → students.id | many enrollments → one student | JOIN students ON enrollments.student_id = students.id |
| enrollments.course_id → courses.id | many enrollments → one course | JOIN courses ON enrollments.course_id = courses.id |
| job_offers.student_id → students.id | many job offers → one student | JOIN students ON job_offers.student_id = students.id |
In short: each student belongs to one cohort. Students enroll in courses through enrollments, and their job offers land in job_offers.
So far every query returned individual rows. Analytics is mostly about summaries: totals, averages, counts per group. That is GROUP BY.
Aggregate functions
An aggregate collapses many rows into one value:
| Function | Returns |
|---|---|
COUNT(*) | number of rows |
COUNT(col) | number of non-NULL values |
COUNT(DISTINCT col) | number of unique values |
SUM(col) | total |
AVG(col) | average |
MIN(col) / MAX(col) | smallest / largest |
Without grouping, an aggregate summarizes the whole table:
SELECT COUNT(*) AS offers, AVG(salary_usd) AS avg_salary
FROM job_offers;
Note: aggregates skip NULLs. AVG(score) averages only completed enrollments.
GROUP BY
GROUP BY splits rows into groups and runs the aggregate per group:
SELECT country, COUNT(*) AS student_count
FROM students
GROUP BY country;
The golden rule: every column in SELECT must be either inside an aggregate or listed in GROUP BY. Postgres enforces this — the error message will become an old friend.
Group by several columns to get finer groups:
SELECT category, difficulty, COUNT(*)
FROM courses
GROUP BY category, difficulty;
HAVING: filtering groups
WHERE filters rows before grouping. It cannot use aggregates. To filter the groups themselves, use HAVING:
SELECT category, COUNT(*) AS course_count
FROM courses
GROUP BY category
HAVING COUNT(*) > 2;
Both can appear in one query — and now you know the full order of clauses:
SELECT city, AVG(salary_usd) AS avg_salary
FROM job_offers
WHERE remote = false -- 1. filter rows
GROUP BY city -- 2. group them
HAVING COUNT(*) >= 2 -- 3. filter groups
ORDER BY avg_salary DESC -- 4. sort
LIMIT 5; -- 5. cut
This single shape — filter, group, filter groups, sort, limit — answers a huge share of real business questions. Congratulations: you have finished Basics of selection I.
Practice
A real Postgres database runs in your browser. Solve the tasks, or just experiment.