PostgreSQL "column must appear in the GROUP BY clause or be used in an aggregate function"
Fix the GROUP BY error in PostgreSQL: understand why it happens, then aggregate the column, group by it or by the primary key, or pick one row per group with DISTINCT ON.
by OhSudo ·
Checked on real software: PostgreSQL 18.6 · November 2026How we check
Part of a learning path:SQL and PostgreSQL, step by step
You want the number of employees per department, with their names, and PostgreSQL refuses:
-- ERROR: must appear in the GROUP BY clause or be used in an aggregate functionSELECT d.name, e.first_name, count(*)FROM departments dJOIN employees e ON e.department_id = d.idGROUP BY d.name;ERROR: column "e.first_name" must appear in the GROUP BY clause or be used in an aggregate functionGROUP BY d.name turns all the employees of a department into one row. count(*) knows what to do with them — it counts. But e.first_name has many values in each group (Emma, Liam, Sara…), and the query doesn’t say which one should appear in that single row. Instead of picking one at random, PostgreSQL stops.
So decide what you actually want for that column. There are four answers. Every query below runs in the SQL Playground on the sample company database.
TL;DR
| You want… | Do this |
|---|---|
| All the values, combined | Aggregate them: string_agg, array_agg, min, max |
| One row per combination | Add the column to GROUP BY |
| Other columns of the same row as the group | GROUP BY the primary key |
| One specific row per group (the highest, the newest) | DISTINCT ON or a window function |
1. Aggregate the column
If you want every name in the department’s row, combine them:
SELECT d.name, count(*) AS employees, string_agg(e.first_name, ', ' ORDER BY e.first_name) AS namesFROM departments dJOIN employees e ON e.department_id = d.idGROUP BY d.nameORDER BY d.name;array_agg gives an array instead of text; min() / max() give one value (the alphabetically first name, the highest salary).
2. Group by the column too
If you want one row per department and per value — for example, how many people per department and job title — the column belongs in GROUP BY:
3. Group by the primary key
You often want more columns of the grouped table — the department’s name and its location. Listing all of them in GROUP BY works, but there’s a shorter way: group by the primary key. PostgreSQL knows that every other column of that table has exactly one value per key, so it allows them:
SELECT d.id, d.name, d.location, count(e.id) AS employees, round(avg(e.salary), 2) AS avg_salaryFROM departments dLEFT JOIN employees e ON e.department_id = d.idGROUP BY d.idORDER BY d.id;(LEFT JOIN and count(e.id) keep departments with no employees, counted as 0.)
This works only for the primary key of a table in the query — not for any unique-looking column like d.name.
4. One row per group: DISTINCT ON
“The highest-paid employee in each department” isn’t an aggregate. You want a whole row, chosen by an order. PostgreSQL’s DISTINCT ON keeps the first row of each group, in the order you give:
SELECT DISTINCT ON (d.id) d.name AS department, e.first_name, e.last_name, e.salaryFROM departments dJOIN employees e ON e.department_id = d.idORDER BY d.id, e.salary DESC;The same with a window function, which also works in other databases:
The same rule for ORDER BY and HAVING
ORDER BY and HAVING run after grouping, so they follow the same rule — you can use grouped columns and aggregates, not single rows’ values:
-- ERROR: must appear in the GROUP BY clause or be used in an aggregate functionSELECT d.name, count(*)FROM departments dJOIN employees e ON e.department_id = d.idGROUP BY d.nameORDER BY e.salary DESC;Order by an aggregate instead — ORDER BY max(e.salary) DESC — and filter groups with HAVING, single rows with WHERE:
SELECT d.name, count(*) AS active_employeesFROM departments dJOIN employees e ON e.department_id = d.idWHERE e.activeGROUP BY d.nameHAVING count(*) >= 2ORDER BY max(e.salary) DESC;Note
MySQL and SQLite. MySQL 5.7+ enforces the same rule by default (ONLY_FULL_GROUP_BY), with a different message: Expression #2 of SELECT list is not in GROUP BY clause and contains nonaggregated column … which is not functionally dependent on columns in GROUP BY clause. Turning that mode off makes MySQL return an arbitrary value — the bug just becomes silent. SQLite allows such columns too and takes them from some row of the group. The SQL Dialect Reference compares grouping across databases.
Shell, we have a problem!
Similar, but different: aggregate functions are not allowed in WHERE — you filtered on count(*) or sum() in WHERE; move that condition to HAVING. column reference "id" is ambiguous — two joined tables have an id; write d.id or e.id. To get used to GROUP BY and HAVING, try the SQL Exercises.
SELECT * FROM practice;
Practice this in the SQL Playground
Open a real PostgreSQL 18.6 database in your browser with this article’s sample data loaded.
Found this useful? Share it.
Found a mistake? Let us know at contact@ohsudo.com.