Skip to content
ohsudo, Home
Databases

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.

Beginner3 min readApplies to PostgreSQL 12–18 · MySQL 8 (ONLY_FULL_GROUP_BY) · SQL in general

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 function
SELECT d.name, e.first_name, count(*)
FROM departments d
JOIN employees e ON e.department_id = d.id
GROUP BY d.name;
ERROR: column "e.first_name" must appear in the GROUP BY clause or be used in an aggregate function

GROUP 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 names
FROM departments d
JOIN employees e ON e.department_id = d.id
GROUP BY d.name
ORDER 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:

SELECT d.name, e.job_title, count(*) AS employees
FROM departments d
JOIN employees e ON e.department_id = d.id
GROUP BY d.name, e.job_title
ORDER BY d.name, e.job_title;

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_salary
FROM departments d
LEFT JOIN employees e ON e.department_id = d.id
GROUP BY d.id
ORDER 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.salary
FROM departments d
JOIN employees e ON e.department_id = d.id
ORDER BY d.id, e.salary DESC;

The same with a window function, which also works in other databases:

SELECT department, first_name, last_name, salary
FROM (
SELECT d.name AS department, e.first_name, e.last_name, e.salary,
row_number() OVER (PARTITION BY d.id ORDER BY e.salary DESC) AS rank
FROM departments d
JOIN employees e ON e.department_id = d.id
) ranked
WHERE rank = 1
ORDER BY department;

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 function
SELECT d.name, count(*)
FROM departments d
JOIN employees e ON e.department_id = d.id
GROUP BY d.name
ORDER 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_employees
FROM departments d
JOIN employees e ON e.department_id = d.id
WHERE e.active
GROUP BY d.name
HAVING count(*) >= 2
ORDER 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.

Open the SQL Playground →

Found this useful? Share it.

Found a mistake? Let us know at contact@ohsudo.com.

Tools

Try it yourself

Articles

Related articles