Window Functions vs GROUP BY: When to Use Each
GROUP BY collapses rows into groups and returns one row per group. Window functions calculate aggregates across a set of rows without collapsing them. Use GROUP BY when you want one row per group. Use window functions when you want to keep all rows while calculating aggregates.
This confusion is common. Developers learn GROUP BY first, then encounter window functions and wonder why they exist. They both calculate sums and averages, so what is the difference? The difference is fundamental and changes how you approach data problems. Once you understand when to reach for each one, you will write cleaner queries and avoid the common mistake of using subqueries where a window function would be simpler and faster.
GROUP BY: Collapse Rows
GROUP BY takes multiple rows and collapses them into one row per group. The result has fewer rows than the input.
-- Total orders per user
SELECT user_id, COUNT(*) as order_count, SUM(total) as total_spent
FROM orders
GROUP BY user_id;
-- Result: One row per user
-- user_id | order_count | total_spent
-- 1 | 5 | 750.00
-- 2 | 3 | 420.00
-- 3 | 8 | 1200.00The original orders table might have 100 rows. After GROUP BY, you have 3 rows. You lost the individual order details. That is the tradeoff: you get summaries, but you lose detail.
Window Functions: Keep All Rows
Window functions calculate aggregates across a set of rows, but they keep every row in the result. The result has the same number of rows as the input.
-- Total orders per user, but keep all order details
SELECT
user_id,
id as order_id,
total,
COUNT(*) OVER (PARTITION BY user_id) as orders_by_user,
SUM(total) OVER (PARTITION BY user_id) as total_spent_by_user
FROM orders;
-- Result: All rows preserved
-- user_id | order_id | total | orders_by_user | total_spent_by_user
-- 1 | 1 | 150.00 | 5 | 750.00
-- 1 | 2 | 75.50 | 5 | 750.00
-- 1 | 3 | 200.00 | 5 | 750.00
-- 2 | 4 | 200.00 | 3 | 420.00Every row is preserved. The window function calculates the aggregate over a "window" of rows defined by PARTITION BY, but it does not collapse anything. The aggregate value is repeated on each row within the partition, which is exactly what you need when you want to compare individual rows against their group totals or calculate per-row percentages.
The Key Difference
GROUP BY: 100 rows in, 3 rows out (one per user).
Window function: 100 rows in, 100 rows out (with aggregates attached to each row).
This is not a minor difference. It changes what questions you can answer.
When to Use GROUP BY
GROUP BY is the right choice when you want:
- Summary reports (total sales per region, average score per student)
- Data aggregation for dashboards (daily active users, monthly revenue)
- Reports that show one row per category
- Any query where you want fewer rows, not the same number of rows
-- Monthly revenue report
SELECT
DATE_FORMAT(created_at, '%Y-%m') as month,
COUNT(*) as orders,
SUM(total) as revenue
FROM orders
GROUP BY DATE_FORMAT(created_at, '%Y-%m')
ORDER BY month;
-- Result: One row per month
-- month | orders | revenue
-- 2026-01 | 150 | 22500.00
-- 2026-02 | 180 | 27000.00
-- 2026-03 | 200 | 30000.00When to Use Window Functions
Window functions are the right choice when you want:
- Ranking rows within groups (top 3 orders per user)
- Running totals (cumulative revenue over time)
- Comparing each row to a group average (above-average orders)
- Accessing previous/next rows (day-over-day change)
- Any query where you need aggregates but also need the original rows
-- Rank orders within each user
SELECT
user_id,
id as order_id,
total,
RANK() OVER (PARTITION BY user_id ORDER BY total DESC) as ranking
FROM orders;
-- Result: All rows with rank
-- user_id | order_id | total | ranking
-- 1 | 3 | 200.00 | 1
-- 1 | 1 | 150.00 | 2
-- 1 | 2 | 75.50 | 3
-- 2 | 4 | 200.00 | 1Running Totals with Window Functions
Running totals are impractical with GROUP BY but trivial with window functions.
-- Running total of revenue per user
SELECT
user_id,
id as order_id,
total,
SUM(total) OVER (PARTITION BY user_id ORDER BY created_at) as running_total
FROM orders
ORDER BY user_id, created_at;
-- Result:
-- user_id | order_id | total | running_total
-- 1 | 1 | 150.00 | 150.00
-- 1 | 2 | 75.50 | 225.50
-- 1 | 3 | 200.00 | 425.50
-- 2 | 4 | 200.00 | 200.00The ORDER BY inside the window function defines the running order. Each row sees all previous rows in the partition. This is useful for cumulative metrics like running totals, moving averages, or calculating the difference between a row and the previous row. Without the ORDER BY inside the window, the function aggregates all rows in the partition at once rather than building up incrementally.
Combining Both
You can use GROUP BY and window functions in the same query. This is powerful for reports that need both summaries and rankings.
-- Users with their total spending and rank
SELECT
user_id,
total_spent,
RANK() OVER (ORDER BY total_spent DESC) as spending_rank
FROM (
SELECT user_id, SUM(total) as total_spent
FROM orders
GROUP BY user_id
) user_totals;
-- Result:
-- user_id | total_spent | spending_rank
-- 3 | 1200.00 | 1
-- 1 | 750.00 | 2
-- 2 | 420.00 | 3Performance Considerations
Window functions can be slower than GROUP BY for simple aggregations because they process more rows. But the performance difference is usually negligible for typical query sizes.
The real performance concern is the ORDER BY inside window functions. If you are sorting millions of rows within each partition, it can be slow. Make sure the columns used in PARTITION BY and ORDER BY are indexed. Without proper indexes, MySQL may need to sort the entire result set in memory, which becomes a bottleneck once the data set grows beyond available buffer pool size.
-- Index for window function performance
CREATE INDEX idx_orders_user_created ON orders(user_id, created_at);Comparison Table: GROUP BY vs Window Functions
| Feature | GROUP BY | Window Functions |
|---|---|---|
| Row Count | Fewer rows (one per group) | Same number of rows |
| Best For | Summary reports, dashboards | Rankings, running totals, comparisons |
| Access to Individual Rows | Lost after aggregation | Preserved |
| HAVING Clause | Supported | Not supported, use subquery or CTE |
| Complexity | Simple syntax | More verbose, but more powerful |
Common Mistakes
Mistake 1: Using GROUP BY When You Need Row-Level Detail
Developers often reach for GROUP BY out of habit, then lose the individual row data they actually need. If your report requires both aggregates and individual rows, use window functions. For example, showing each order alongside the user's total spending requires a window function, not GROUP BY.
Mistake 2: Forgetting PARTITION BY in Window Functions
Without PARTITION BY, the window function calculates across all rows in the table. If you want per-user totals but forget PARTITION BY user_id, you get a grand total instead. Always verify your window function scope by checking the PARTITION BY clause.
Mistake 3: Using RANK Instead of ROW_NUMBER for Unique Ordering
RANK assigns the same rank to tied values, creating gaps. If you need exactly one row per group (like the top order per user), use ROW_NUMBER instead. RANK with ties will return multiple rows for the same rank, which can break application logic expecting a single result.
The choice between GROUP BY and window functions comes down to whether you need the original rows in your result. If you want a summary report, GROUP BY is the right tool. If you need to see individual rows alongside their aggregates, window functions are the way to go. Mastering both gives you the flexibility to write efficient queries for any reporting scenario.
Key Takeaways
- GROUP BY reduces the number of output rows, window functions preserve the original row count.
- Use window functions when you need aggregates alongside individual row data.
- Always include PARTITION BY to control the scope of window function calculations
- Index columns used in PARTITION BY and ORDER BY for window function performance
- Combine GROUP BY and window functions when reports need both summaries and rankings
FAQ
Can I use window functions without GROUP BY?
Yes. Window functions work independently of GROUP BY. You can use them in any SELECT statement. GROUP BY is optional and used only when you also want to aggregate rows.
Which is faster, GROUP BY or window functions?
For simple aggregations, GROUP BY is slightly faster because it processes fewer rows. For complex analytical queries, window functions are often faster because they avoid subqueries and self-joins.
Do all SQL databases support window functions?
MySQL 8.0+, PostgreSQL, SQL Server, and Oracle all support window functions. SQLite added support in version 3.25.0. If you are using an old MySQL version (before 8.0), you cannot use window functions.
What is the difference between RANK, DENSE_RANK, and ROW_NUMBER?
ROW_NUMBER assigns unique sequential numbers (1, 2, 3). RANK allows gaps for ties (1, 1, 3). DENSE_RANK does not allow gaps (1, 1, 2). Use ROW_NUMBER for unique ordering, RANK for leaderboard-style ranking, and DENSE_RANK when you want consecutive ranks.
Can I use a window function in a WHERE clause?
No. Window functions are evaluated after WHERE. To filter on window function results, use a subquery or CTE. For example, to find orders ranked in the top 3, wrap the query with the RANK() window function in a subquery and filter the outer query on the rank column.
What is the difference between OVER() and PARTITION BY?
OVER() defines the window. PARTITION BY splits the data into groups within that window. If you use OVER() without PARTITION BY, the function operates on the entire result set. With PARTITION BY, the function resets for each partition. For example, SUM(total) OVER(PARTITION BY user_id) calculates a per-user total, while SUM(total) OVER() calculates a grand total.
Written by
MasterSQL