Windows vs GROUP BY
A window function computes an aggregate-like value per row without collapsing the result set. GROUP BY collapses; OVER() keeps every row. Reach for a window when you need detail rows and an aggregate in the same query.
The dashboard ticket reads: “show every sale, and next to each one, what percent of its region’s total it was.” A junior writes a GROUP BY region, gets four rows back, and realizes with dread that the detail rows are gone. The senior writes one OVER (PARTITION BY region) and ships it in a line. Same arithmetic, opposite result shape — and the difference is the most useful trick in analytic SQL. In the next ten minutes you’ll understand exactly why GROUP BY loses the rows and how OVER keeps them, so the next time a report demands “each row plus a group aggregate” you’ll reach for the right tool immediately.
GROUP BY collapses, the window keeps every row
GROUP BY is a reducer. You hand it many rows, it returns one row per group. After GROUP BY region you have four rows — one per region — and there is no way to also see the individual sales, because they were folded into the aggregate. That is the whole point of grouping: collapse detail into a summary.
A window function runs the same aggregate math but emits the result alongside every original row. Nothing collapses. The OVER (...) clause is what makes a function a window function: it tells Postgres “compute this over a set of related rows — the window — but attach the answer to each row instead of folding rows together.”
-- GROUP BY: 4 rows out, detail gone
SELECT region, SUM(amount) AS region_total
FROM sales
GROUP BY region;
-- Window: every sale row kept, region total attached to each
SELECT region, salesperson, sale_date, amount,
SUM(amount) OVER (PARTITION BY region) AS region_total
FROM sales;The first query answers “what did each region sell?” The second answers “show me each sale and its region’s total on the same line” — which is exactly what the dashboard ticket needed. Same SUM, same region grouping logic; the only structural difference is OVER (PARTITION BY region) instead of GROUP BY region.
The percent-of-total pattern, in one query
Because the window keeps every row, you can mix a detail value and an aggregate in the same SELECT and divide them — no self-join, no subquery:
SELECT region, salesperson, amount,
ROUND(
100.0 * amount / SUM(amount) OVER (PARTITION BY region),
1
) AS pct_of_region
FROM sales
ORDER BY region, pct_of_region DESC;amount is the row’s own value; SUM(amount) OVER (PARTITION BY region) is its region’s total computed across all rows in the same region. Divide and you get each sale’s share of its region — per row. With plain GROUP BY this requires computing the totals separately and joining them back, which is more code and a second pass over the data.
When to use which — the decision rule
Ask yourself: does the requirement mention “each row” or “per row” next to an aggregate? That single question determines the tool.
The choice is not about which is “better”; it is about the shape of the answer you need:
- Need a summary only (one row per group)? Use
GROUP BY. Reports, counts per status, daily totals. - Need detail rows and an aggregate on the same row? Use a window. Percent-of-total, rank within group, running balance, “how does this row compare to its group’s average.”
A reliable production rule: if the word “each” or “per row” appears in the requirement next to an aggregate, you want a window. “Show each order with its customer’s lifetime total” — window. “Show the total per customer” — GROUP BY. Mixing them up is the classic junior bug: reaching for GROUP BY and then discovering the detail rows you needed are gone, or trying to put a non-aggregated column in a GROUP BY query and hitting column must appear in the GROUP BY clause.
▸Why this works
Why can’t you just SELECT salesperson, SUM(amount) ... GROUP BY region and get both? Because once rows are grouped, salesperson is ambiguous — a region has many salespeople, so which one’s name would the single output row carry? SQL refuses, demanding every selected column be either grouped or aggregated. Windows sidestep this entirely: rows are never collapsed, so salesperson stays unambiguous on its own row while SUM(amount) OVER (...) rides alongside. The aggregation logic in unit 03-aggregation and the windowing logic here are two answers to “summarize,” distinguished only by whether detail survives.
You run SELECT region, salesperson, SUM(amount) OVER (PARTITION BY region) FROM sales; against a table of 8 sales across 4 regions. How many rows come back?
A requirement says: 'list every event with the running count of events for that user.' Which tool fits and why?
Order these steps to add 'percent of region total' to every sale row:
- 1 Start from the detail query: SELECT region, salesperson, amount FROM sales
- 2 Add SUM(amount) OVER (PARTITION BY region) — the region total, kept per row
- 3 Divide the row's amount by that windowed total
- 4 Multiply by 100.0 and ROUND to get a readable percent
- 5 ORDER BY region, pct DESC to present it
- 01In one sentence, what is the structural difference between GROUP BY and a window function?
- 02You need every order shown next to its customer's lifetime total. GROUP BY or window — and why?
- 03Why does SELECT salesperson, SUM(amount) ... GROUP BY region fail, but the same with OVER (PARTITION BY region) works?
GROUP BY and window functions answer the same “summarize” question with opposite result shapes. GROUP BY is a reducer — it folds detail rows into one row per group, so afterward the individual rows are gone. A window function adds OVER (...) to an aggregate so the same math is computed over a window of related rows but the answer is attached to every original row; the row count is unchanged. That row-count invariant is the decision rule: if the requirement needs both detail rows and an aggregate — percent of total, rank within group, running balance, comparison to the group average — you want a window. If it needs only a summary, GROUP BY is simpler. Now when you see a ticket that says “show each X with its group’s Y,” reach for OVER (PARTITION BY ...) before you write a single GROUP BY — the detail rows will still be there. The next lesson opens up the two clauses that shape a window: PARTITION BY, which restarts the window per group, and ORDER BY inside OVER, which orders rows within it — and quietly changes what the window contains.
Practice
Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.
Something unclear?
Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.