PostgreSQLMySQLSQL Server

column must appear in the GROUP BY clause

You selected a column that isn't grouped and isn't wrapped in an aggregate. Postgres, MySQL (in strict mode), and SQL Server all refuse to guess which row's value you meant.

The exact wording differs by engine, but it's the same complaint everywhere:

  • PostgreSQL: ERROR: column "orders.order_date" must appear in the GROUP BY clause or be used in an aggregate function
  • MySQL (with ONLY_FULL_GROUP_BY, the default since 5.7): Expression #2 of SELECT list is not in GROUP BY clause and contains nonaggregated column 'orders.order_date' which is not functionally dependent on columns in GROUP BY clause
  • SQL Server: Column 'orders.order_date' is invalid in the select list because it is not contained in either an aggregate function or the GROUP BY clause.

The cause

GROUP BY collapses many rows into one per group. Every column in SELECT has to be either in the GROUP BY list, or wrapped in an aggregate (COUNT, SUM, MIN, MAX, AVG, ...) so the engine knows how to reduce many values down to one. If you select a plain column that's neither, the engine has no rule for which of the group's rows to take it from — so it refuses.

This is the classic version:

-- customer_id has many order_date values per group — which one wins?
SELECT customer_id, order_date, COUNT(*)
FROM orders
GROUP BY customer_id;

The fix

Pick one of two honest answers:

You don't actually need that column — drop it:

SQL playground
Loading editor…
⌘/Ctrl + Enter

You need a specific row's value from the group — say so explicitly with an aggregate, instead of leaving it to chance:

SQL playground
Loading editor…
⌘/Ctrl + Enter

If you need the whole row for whichever order is most recent (not just one column from it), aggregating won't get you there — see Top N per group for the window-function pattern that does.

The SQLite trap: it won't even tell you

This is the one place SQLite is more dangerous than the strict engines, not less. It has no ONLY_FULL_GROUP_BY-style check — it silently picks a value from an arbitrary row in the group and moves on:

SQL playground
Loading editor…
⌘/Ctrl + Enter

That ran without a single warning, and order_date looks like a real answer — but which row it came from isn't defined by the query, only by whatever order SQLite happened to scan the table in. Change an index, add a row, or upgrade SQLite, and the value can change out from under you with no error to flag it. If you write this in SQLite and it "works," test the same query against Postgres or MySQL before you trust it — they'll catch what SQLite won't.

  • GROUP BY — the full rules for what can and can't appear in the SELECT list.
  • HAVING — filtering after grouping, the other half of this clause pair.
  • Top N per group — when you need a whole row per group, not just one aggregated column.
  • COUNT — the aggregate you'll reach for most often here.
See the arbitrary-row behavior for yourself — rerun the last query a few times, or add more rows to orders and watch which order_date comes back.Open the playground →