What window functions do
A window function performs a calculation across a set of rows related to the
current row — a ranking, a running total, the previous row’s value — without
collapsing the result the way GROUP BY does. Every input row stays in the
output. This reference lists the ranking, offset and value functions plus the
OVER() clause they all share, with per-database support.
How it works
The OVER() clause defines the window: how rows are partitioned, ordered, and
framed:
SELECT name, department, salary,
ROW_NUMBER() OVER (PARTITION BY department ORDER BY salary DESC) AS rn,
RANK() OVER (PARTITION BY department ORDER BY salary DESC) AS rnk,
LAG(salary) OVER (PARTITION BY department ORDER BY salary DESC) AS prev,
SUM(salary) OVER (PARTITION BY department
ORDER BY salary
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS running
FROM employees;
PARTITION BY resets the calculation per group, ORDER BY orders rows within
each partition, and the frame clause bounds the rows an aggregate or value
function sees. Ranking and offset functions require ORDER BY. The tool filters
the table as you type and flags dialect support.
Choosing between ROW_NUMBER, RANK, and DENSE_RANK
These three ranking functions look similar but handle ties differently — a detail that matters whenever you want “the top 3 per group” or “the most recent entry per user”:
| Function | Tie behaviour | Example output (scores 90, 90, 80) |
|---|---|---|
ROW_NUMBER() | Assigns unique numbers, breaks ties arbitrarily | 1, 2, 3 |
RANK() | Same rank for ties, then skips numbers | 1, 1, 3 |
DENSE_RANK() | Same rank for ties, no gaps | 1, 1, 2 |
Use ROW_NUMBER() when you need exactly one row per partition (for example, the latest order per customer). Use DENSE_RANK() when you want “everyone in the top 3 positions” including all tied entries.
Frame clauses for running totals and moving averages
The frame clause controls which rows an aggregate window sees relative to the current row. Without an explicit frame, an ordered window defaults to RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW — which gives a running total but groups peer rows:
-- Running total (default frame):
SUM(amount) OVER (ORDER BY txn_date)
-- 7-day moving average (explicit ROWS frame):
AVG(daily_revenue) OVER (
ORDER BY day
ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
)
ROWS counts physical rows; RANGE groups rows with identical ORDER BY values. For time-series moving averages, always use ROWS to get a predictable count.
Tips and notes
- Window functions run after
WHERE/GROUP BY— you cannot filter on them inWHERE; use a subquery orQUALIFY(where supported). ROWSframes count physical rows;RANGEframes group peer rows with equalORDER BYvalues.- The default frame for an ordered window is
RANGE UNBOUNDED PRECEDING TO CURRENT ROW. LAG/LEADtake optional offset and default arguments:LAG(x, 2, 0).- Named windows (
WINDOW w AS (PARTITION BY department ORDER BY salary)) avoid repeating the sameOVER()clause on every function in a query.