PARTITION BY is GROUP BY for windows. It re-starts the ranking, the running
total, or the rolling average every time the partition column changes. That’s
how you compute running team totals through the season or per-team top
receivers in one query.
Concept
SUM(col) OVER (
PARTITIONBY group_col
ORDER BY sort_col
ROWSBETWEENUNBOUNDEDPRECEDINGAND CURRENT ROW
)
The frame clause (ROWS BETWEEN ...) controls which rows in the partition the
function sees. Two patterns cover 95% of cases:
Running total — UNBOUNDED PRECEDING AND CURRENT ROW (everything from
the start of the partition through this row). The default when you provide
ORDER BY without an explicit frame.
Rolling window — n PRECEDING AND CURRENT ROW (last n+1 rows). Use for
3-game moving averages, 5-game form, etc.
AVG(rushing_yards) OVER (
PARTITIONBY player_display_name
ORDER BYweek
ROWSBETWEEN2PRECEDINGAND CURRENT ROW
) -- 3-game rolling average per player
Lions example
Lions weekly points scored, with a running season total and a 3-game rolling
average:
WITH lions_games AS (
SELECT
week,
CASEWHEN home_team ='DET'THEN home_score ELSE away_score ENDAS pts
FROM schedules
WHERE season =2024
AND game_type ='REG'
AND (home_team ='DET'OR away_team ='DET')
)
SELECT
week,
pts,
SUM(pts) OVER (ORDER BYweek) AS season_total,
ROUND(AVG(pts) OVER (
ORDER BYweek
ROWSBETWEEN2PRECEDINGAND CURRENT ROW
)::numeric, 1) AS rolling_3_avg
FROM lions_games
ORDER BYweek;
You get 18 rows, each with this week’s points, the running total through this
week, and the 3-game average.
Try it
For every Lions player in 2024, compute their running rushing yards total
through the season — one row per player-week, with cumulative yards in a
new column. Sort by player, then week.
Common mistakes
Forgetting ORDER BY inside OVER for running totals. Without it,
Postgres treats the whole partition as one frame, so every row gets the same
total. The ORDER BY is what makes “running” mean “through this row.”
Default frame surprise. When you write SUM(...) OVER (ORDER BY week),
the default frame is RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW —
not ROWS. Usually fine, but watch out when sort values tie.
PARTITION BY typo. Partitioning by the wrong column silently produces
the wrong answer. Always sanity-check by inspecting the rows of one
partition.
Aggregate window without PARTITION BY. That’s a total across all rows
in the result set — useful, but not what you want for per-team / per-player
splits.
Quick check
What does PARTITION BY do that GROUP BY doesn’t?
What’s the frame clause for a 5-game rolling average?
Why does SUM(col) OVER () (no partition, no order) just compute one value
for every row?