Aggregate Window Functions
Use COUNT, SUM, AVG, MIN and MAX with OVER for part-to-whole percentages, NULL-aware averages and row-vs-aggregate comparisons.
The five aggregate functions you already know become far more powerful with OVER: each innings can be compared with its player's total, the season average, or the best score — all in one query, without losing a single row.
Definition
An aggregate window function is a normal aggregate (COUNT, SUM, AVG, MIN, MAX) used with an OVER() clause. It computes the aggregate over the row's window and returns the result on every row, so detail values and summary values can be used together in the same row.
- The NULL rules are exactly the same as for normal aggregates:
COUNT(*)counts all rows, everything else ignores NULL. - PARTITION BY, ORDER BY and frames are all optional for these functions.
Syntax
sql
COUNT(* | expression) OVER ([PARTITION BY ...] [ORDER BY ... [frame]])
SUM(expression) OVER ([PARTITION BY ...] [ORDER BY ... [frame]])
AVG(expression) OVER ([PARTITION BY ...] [ORDER BY ... [frame]])
MIN(expression) OVER ([PARTITION BY ...] [ORDER BY ... [frame]])
MAX(expression) OVER ([PARTITION BY ...] [ORDER BY ... [frame]])Example — COUNT(*) vs COUNT(runs) in a window
sql
SELECT id, player, runs,
COUNT(*) OVER (PARTITION BY player) AS all_rows,
COUNT(runs) OVER (PARTITION BY player) AS with_runs
FROM innings;| id | player | runs | all_rows | with_runs |
|---|---|---|---|---|
| 1 | Virat Kohli | 82 | 3 | 3 |
| 2 | Virat Kohli | 59 | 3 | 3 |
| 3 | Virat Kohli | 73 | 3 | 3 |
| 4 | Rohit Sharma | 45 | 3 | 3 |
| 5 | Rohit Sharma | 76 | 3 | 3 |
| 6 | Rohit Sharma | 0 | 3 | 3 |
| 7 | MS Dhoni | 26 | 3 | 2 |
| 8 | MS Dhoni | NULL | 3 | 2 |
| 9 | MS Dhoni | 45 | 3 | 2 |
| 10 | Jos Buttler | 73 | 3 | 3 |
| 11 | Jos Buttler | 59 | 3 | 3 |
| 12 | Jos Buttler | 107 | 3 | 3 |
Every player played 3 innings (COUNT(*)), but Dhoni has only 2 innings with a value (COUNT(runs) skips NULL). Use case: data-quality checks — "which players have missing scores?".
Example — SUM: part-to-whole percentage
sql
SELECT id, player, runs,
ROUND(runs * 100.0 / SUM(runs) OVER (), 1) AS pct_of_total
FROM innings;| id | player | runs | pct_of_total |
|---|---|---|---|
| 1 | Virat Kohli | 82 | 12.7 |
| 2 | Virat Kohli | 59 | 9.1 |
| 3 | Virat Kohli | 73 | 11.3 |
| 4 | Rohit Sharma | 45 | 7 |
| 5 | Rohit Sharma | 76 | 11.8 |
| 6 | Rohit Sharma | 0 | 0 |
| 7 | MS Dhoni | 26 | 4 |
| 8 | MS Dhoni | NULL | NULL |
| 9 | MS Dhoni | 45 | 7 |
| 10 | Jos Buttler | 73 | 11.3 |
| 11 | Jos Buttler | 59 | 9.1 |
| 12 | Jos Buttler | 107 | 16.6 |
Step by step: (1) SUM(runs) OVER () gives the grand total 645 on each row; (2) each row's share = runs × 100 ÷ 645. Buttler's 107 was 16.6% of all runs. 100.0 (not 100) forces decimal division — 82 * 100 / 645 would give 12 in SQL Server. NULL × anything = NULL. (SQL Server shows trailing zeros such as 12.700000; the value is the same.)
Example — AVG per player, and the NULL decision
sql
SELECT id, player, runs,
ROUND(AVG(runs * 1.0) OVER (PARTITION BY player), 1) AS avg_default,
ROUND(AVG(COALESCE(runs, 0) * 1.0) OVER (PARTITION BY player), 1) AS avg_with_zero
FROM innings
WHERE player IN ('Virat Kohli', 'MS Dhoni');| id | player | runs | avg_default | avg_with_zero |
|---|---|---|---|---|
| 1 | Virat Kohli | 82 | 71.3 | 71.3 |
| 2 | Virat Kohli | 59 | 71.3 | 71.3 |
| 3 | Virat Kohli | 73 | 71.3 | 71.3 |
| 7 | MS Dhoni | 26 | 35.5 | 23.7 |
| 8 | MS Dhoni | NULL | 35.5 | 23.7 |
| 9 | MS Dhoni | 45 | 35.5 | 23.7 |
- Default AVG excludes NULL: Dhoni = (26 + 45) ÷ 2 = 35.5.
- COALESCE(runs, 0) counts NULL as 0: Dhoni = (26 + 0 + 45) ÷ 3 = 23.7.
Neither is "wrong" — it is a business decision. "Did not bat" should not lower a batting average, so 35.5 is correct for cricket. But for "average runs per match played", 23.7 is right. Always decide consciously how NULL should be treated. (All four averages: Kohli 71.3, Rohit 40.3, Dhoni 35.5, Buttler 79.7.)
Example — MIN & MAX: extremes per player
sql
SELECT id, player, runs,
MIN(runs) OVER (PARTITION BY player) AS lowest,
MAX(runs) OVER (PARTITION BY player) AS highest
FROM innings;Kohli: lowest 59, highest 82 · Rohit: 0 / 76 · Dhoni: 26 / 45 · Buttler: 59 / 107. MIN/MAX work on any data type — numbers, text and dates (e.g. MIN(match_date) OVER (PARTITION BY player) = each player's first match).
Example — compare each row with an aggregate
sql
SELECT id, player, runs,
MAX(runs) OVER () AS best,
MAX(runs) OVER () - runs AS gap_to_best
FROM innings;| id | player | runs | best | gap_to_best |
|---|---|---|---|---|
| 1 | Virat Kohli | 82 | 107 | 25 |
| 2 | Virat Kohli | 59 | 107 | 48 |
| 3 | Virat Kohli | 73 | 107 | 34 |
| 4 | Rohit Sharma | 45 | 107 | 62 |
| 5 | Rohit Sharma | 76 | 107 | 31 |
| 6 | Rohit Sharma | 0 | 107 | 107 |
| 7 | MS Dhoni | 26 | 107 | 81 |
| 8 | MS Dhoni | NULL | 107 | NULL |
| 9 | MS Dhoni | 45 | 107 | 62 |
| 10 | Jos Buttler | 73 | 107 | 34 |
| 11 | Jos Buttler | 59 | 107 | 48 |
| 12 | Jos Buttler | 107 | 107 | 0 |
The same pattern works for "distance from the lowest" or "difference from the average".
Example — above or below average? (window + CASE)
sql
SELECT id, player, runs,
ROUND(AVG(runs * 1.0) OVER (), 1) AS avg_all,
CASE WHEN runs > AVG(runs * 1.0) OVER () THEN 'Above' ELSE 'Below' END AS vs_avg
FROM innings;| id | player | runs | avg_all | vs_avg |
|---|---|---|---|---|
| 1 | Virat Kohli | 82 | 58.6 | Above |
| 2 | Virat Kohli | 59 | 58.6 | Above |
| 3 | Virat Kohli | 73 | 58.6 | Above |
| 4 | Rohit Sharma | 45 | 58.6 | Below |
| 5 | Rohit Sharma | 76 | 58.6 | Above |
| 6 | Rohit Sharma | 0 | 58.6 | Below |
| 7 | MS Dhoni | 26 | 58.6 | Below |
| 8 | MS Dhoni | NULL | 58.6 | Below |
| 9 | MS Dhoni | 45 | 58.6 | Below |
| 10 | Jos Buttler | 73 | 58.6 | Above |
| 11 | Jos Buttler | 59 | 58.6 | Above |
| 12 | Jos Buttler | 107 | 58.6 | Above |
The overall average is 645 ÷ 11 = 58.6. Window + CASE gives instant labels. Careful: NULL > 58.6 is not TRUE, so the NULL row falls into the ELSE → 'Below' branch. Add WHEN runs IS NULL THEN 'Did not bat' first if that matters.
Window functions on top of GROUP BY
You can combine both: GROUP BY first makes one row per team, then a window function works on those grouped rows.
sql
SELECT team,
SUM(runs) AS team_runs,
ROUND(SUM(runs) * 100.0 / SUM(SUM(runs)) OVER (), 1) AS pct,
RANK() OVER (ORDER BY SUM(runs) DESC) AS team_rank
FROM innings
GROUP BY team;| team | team_runs | pct | team_rank |
|---|---|---|---|
| RR | 239 | 37.1 | 1 |
| RCB | 214 | 33.2 | 2 |
| MI | 121 | 18.8 | 3 |
| CSK | 71 | 11 | 4 |
SUM(SUM(runs)) OVER () = the inner SUM is the GROUP BY aggregate per team, the outer SUM … OVER () adds those team totals (645).
Common mistakes
- ❌
runs * 100 / SUM(runs) OVER ()→ integer division in SQL Server ✅ use100.0. - ❌ Forgetting that AVG skips NULL → ✅ decide: skip (default) or count as 0 with
COALESCE(runs, 0). - ❌ CASE without a NULL branch → ✅ NULL comparisons are never true, so NULLs land in ELSE.
Interview tip
Q: How do you show each row's percentage of the total without a subquery?
A: value * 100.0 / SUM(value) OVER () — the window SUM puts the grand total on every row. Use PARTITION BY for "percentage of the group".
Key points to remember
- COUNT, SUM, AVG, MIN, MAX + OVER() = aggregate window functions: the aggregate appears on every row.
- NULL rules are unchanged: COUNT(*) counts all rows, the others ignore NULL (Dhoni: 3 rows, 2 with runs).
- Part-to-whole: runs * 100.0 / SUM(runs) OVER () — use 100.0 to avoid integer division.
- AVG ignores NULL (Dhoni 35.5); AVG(COALESCE(runs, 0)) counts it as 0 (23.7) — choose deliberately.
- Compare each row with an aggregate: MAX(runs) OVER () - runs = gap to the best score.
- Window + CASE labels rows (Above/Below average); NULL falls into the ELSE branch.
- Window functions can run on GROUP BY results, e.g. SUM(SUM(runs)) OVER () or RANK() OVER (ORDER BY SUM(runs) DESC).
Try it yourself
Part-to-whole and per-player share
Runs on a fresh IPL practice database in your browser · edit the query and press Ctrl + Enter
AVG: ignore NULL vs count it as 0
Runs on a fresh IPL practice database in your browser · edit the query and press Ctrl + Enter
Compare each innings with the best and the average
Runs on a fresh IPL practice database in your browser · edit the query and press Ctrl + Enter
Practice: gap to each player's own best
Runs on a fresh IPL practice database in your browser · edit the query and press Ctrl + Enter
Revision Flashcards
Tap a card to flip it and see the answer.
