What GROUP BY actually does at its core

GROUP BY takes the rows surviving WHERE and partitions them into buckets — one bucket per distinct value (or combination of values) in the grouped column(s). That partitioning step is the whole mechanism. What happens to each bucket after that — computing a COUNT(), a SUM(), or nothing at all beyond just keeping the grouped column — is a separate, optional decision. See the full sequence in SQL Execution Order Explained: grouping happens as its own step, before SELECT decides what to compute from each group.

Example: GROUP BY with zero aggregates

This query has no COUNT(), no SUM(), nothing but the grouped column itself — and it's completely valid SQL:

group-by-no-aggregate.sql
SELECT status
FROM orders
GROUP BY status;
-- Returns each distinct status value exactly once:
-- 'pending', 'completed', 'cancelled', 'refunded'

Nothing here is being counted or summed. GROUP BY is doing exactly one thing: collapsing every row down to one row per distinct status value.

GROUP BY vs DISTINCT — when they're identical

Written this way, GROUP BY is functionally identical to SELECT DISTINCT:

group-by-vs-distinct.sql
-- These two return exactly the same rows
SELECT status FROM orders GROUP BY status;
SELECT DISTINCT status FROM orders;

They only diverge once an aggregate enters the picture. DISTINCT has no concept of "per group" — it only removes duplicate rows from whatever SELECT already produced. GROUP BY, on the other hand, can pair the grouped column with a computed value for each group:

where-they-diverge.sql
-- DISTINCT cannot do this — there's no per-group COUNT to attach
SELECT status, COUNT(*) AS total
FROM orders
GROUP BY status;

The rule every non-grouped SELECT column must follow

Once you group by one or more columns, every other column in SELECT must either also be in GROUP BY, or be wrapped in an aggregate function. A column that's neither has no single well-defined value once multiple rows collapse into one group, and most databases will reject the query outright rather than pick an arbitrary row's value for you. This rule, and the errors it produces, is covered in full in GROUP BY Mistakes That Break SQL Queries.

ungrouped-column-error.sql
-- Error in most databases: order_date is neither
-- grouped nor aggregated
SELECT customer_id, order_date, COUNT(*)
FROM orders
GROUP BY customer_id;

When you actually want an aggregate

Deduplication is a legitimate use of GROUP BY on its own, but most real queries pair grouping with at least one aggregate to answer a "how many" or "how much" question per group. For the deeper toolbox once you're past a single COUNT()ROLLUP, CUBE, conditional aggregation — see Advanced GROUP BY Techniques You Should Know, and for the specific nuances of COUNT(*) versus COUNT(column), see COUNT(*) vs COUNT(column).

GROUP BY vs DISTINCT: does it matter for speed?

For a plain deduplication query, most modern optimizers recognize GROUP BY column and DISTINCT column as logically equivalent and generate the same plan for both — run EXPLAIN on each form to confirm on your own database (see EXPLAIN Query Plan – Beginner to Advanced). In practice, the choice between them comes down to readability and intent, not speed: reach for GROUP BY when you expect to add an aggregate later, and DISTINCT when deduplication really is the whole point.

Key takeaways

  • GROUP BY's core function is collapsing rows into one per distinct grouped value — no aggregate required.
  • Without an aggregate, GROUP BY and DISTINCT return identical results.
  • They diverge once you need a per-group computed value, which only GROUP BY can provide.
  • Every SELECT column must be grouped or aggregated — this is the source of most GROUP BY errors.
  • Most optimizers treat GROUP BY-without-aggregate and DISTINCT the same way for performance.