← All cheatsheets
Data Analyst · #022 · August 14, 2026 · 2 min read

GROUP BY vs PARTITION BY: what is the actual difference?

GROUP BY collapses 200 rows into 3. PARTITION BY keeps all 200 and adds group context to each. The two queries side by side, the rule of thumb that picks between them, and the window-filter trap.

Get the free PDF

One page, print-ready, free to share. No signup needed.

Download the PDF

You had 200 rows. Now you have 3, and the query that tried to keep the names threw an error instead:

SELECT dept, name, AVG(salary)
FROM employees
GROUP BY dept;
-- ERROR: 'name' must appear in GROUP BY

The error is the engine telling you that you asked two incompatible questions at once. GROUP BY answers one of them. PARTITION BY answers the other.

Two different questions

"What is the average salary per department?" is a question about groups. Detail rows are irrelevant to the answer, so GROUP BY deletes them:

SELECT dept, AVG(salary) AS avg_salary
FROM employees
GROUP BY dept;
-- 200 employees -> 3 rows

"How does each person compare to their department?" is a question about rows in context. Every row must survive, so it needs a window:

SELECT name, dept, salary,
       AVG(salary) OVER (PARTITION BY dept) AS dept_avg,
       salary - AVG(salary) OVER (PARTITION BY dept) AS vs_dept
FROM employees;
-- all 200 rows, each with its context

This second query is an interview classic precisely because it filters out the candidates who only know GROUP BY: without windows, it takes a self-join.

The rule of thumb

Listen to the question's grammar. The word "per" almost always means GROUP BY. The phrases "vs its", "compared to", "share of" almost always mean PARTITION BY. When the request needs both shapes, aggregate first, then window over the result.

The window-filter trap

The natural next step fails:

-- broken: WHERE runs before windows exist
WHERE salary > dept_avg

-- works: materialize, then filter
WITH ranked AS (
  SELECT ..., AVG(salary) OVER (PARTITION BY dept) AS dept_avg
  FROM employees
)
SELECT * FROM ranked
WHERE salary > dept_avg;

Same execution-order story as WHERE vs HAVING (sheet #018): the clause you are filtering in runs before the value you are filtering on exists. The CTE wrapper is the standard fix everywhere; QUALIFY is the shortcut on the engines that have it.

The takeaway

GROUP BY collapses, PARTITION BY keeps. Pick by the shape of the answer the question needs, and filter windows one layer out. The print-ready PDF above has both queries and the trap on one page.

Frequently asked questions

What is the difference between GROUP BY and PARTITION BY?
GROUP BY collapses rows: one output row per group, detail gone. PARTITION BY defines a window: every row survives and carries an aggregate computed over its group. Same grouping idea, opposite effect on row count.
When should I use PARTITION BY instead of GROUP BY?
When the question compares each row to its group: each employee against their department average, each order against the customer's total, each day against the month. If the question contains "per", GROUP BY usually answers it; if it contains "vs its" or "compared to", you want a window.
Why can't I filter on a window function in WHERE?
Because WHERE runs before window functions are evaluated. The value you are filtering on does not exist yet at that stage. Wrap the query in a CTE or subquery and filter in the outer SELECT, or use QUALIFY on engines that support it.
Can GROUP BY and PARTITION BY be used in the same query?
Yes. A common pattern aggregates first with GROUP BY, then applies a window over the aggregated rows, for example ranking departments by their average salary. The window then partitions over the already-grouped result.

Get the free PDF

One page, print-ready, free to share. No signup needed.

Download the PDF

More cheatsheets