Master advanced analytics with powerful window functions
Window functions perform calculations across a set of rows that are somehow related to the current row, but unlike aggregate functions, they don't collapse rows. Each row keeps its identity while accessing data from other rows in its "window."
| department | avg |
|---|---|
| Sales | 5000 |
| Engineering | 6000 |
⚠️ Rows are collapsed - you lose individual employee data
| name | department | salary | dept_avg |
|---|---|---|---|
| Alice | Sales | 5000 | 5000 |
| Bob | Sales | 5000 | 5000 |
| Charlie | Engineering | 6000 | 6000 |
✓ All rows preserved - you keep individual employee data!
Assigns a unique sequential number to each row
ROW_NUMBER() OVER (ORDER BY salary DESC)
Ranks rows with gaps after ties
RANK() OVER (ORDER BY score DESC)
Ranks rows without gaps
DENSE_RANK() OVER (ORDER BY score DESC)
Access previous row's value
LAG(salary, 1) OVER (ORDER BY date)
Access next row's value
LEAD(price, 1) OVER (ORDER BY date)
Running total or windowed sum
SUM(amount) OVER (ORDER BY date)
Moving average or windowed average
AVG(sales) OVER (ORDER BY month)
Divide rows into n equal groups
NTILE(4) OVER (ORDER BY revenue)
PARTITION BY divides rows into partitions (groups) to which the window function is applied separately.
| name | department | salary | rank_in_dept | dept_avg |
|---|---|---|---|---|
| Charlie | Sales | 5500 | 1 | 5000 |
| Alice | Sales | 5000 | 2 | 5000 |
| Eve | Sales | 4500 | 3 | 5000 |
| Frank | Engineering | 6500 | 1 | 6100 |
| Bob | Engineering | 6000 | 2 | 6100 |
| Grace | Engineering | 5800 | 3 | 6100 |
| Diana | Marketing | 4800 | 1 | 4800 |
Notice how ROW_NUMBER() restarts at 1 for each department!
The PARTITION BY clause creates separate "windows" for Sales, Engineering, and Marketing.
Each partition is ranked independently.
Understanding the difference between ranking functions:
| name | score | ROW_NUMBER | RANK | DENSE_RANK |
|---|---|---|---|---|
| Alice | 95 | 1 | 1 | 1 |
| Bob | 90 | 2 | 2 | 2 |
| Charlie | 90 | 3 | 2 | 2 |
| Diana | 85 | 4 | 4 | 3 |
| Eve | 80 | 5 | 5 | 4 |
| Frank | 80 | 6 | 5 | 4 |
| Grace | 75 | 7 | 7 | 5 |
Always unique (1, 2, 3, 4, 5, 6, 7)
Even with ties, each row gets a unique number
Same rank for ties, skips next (1, 2, 2, 4, 5, 5, 7)
Gaps appear after ties
Same rank for ties, no gaps (1, 2, 2, 3, 4, 4, 5)
No gaps in sequence
Window functions with ORDER BY create cumulative calculations:
| date | sales | running_total | moving_avg_3 |
|---|---|---|---|
| 2024-01-01 | 100 | 100 | 100.0 |
| 2024-01-02 | 150 | 250 | 125.0 |
| 2024-01-03 | 200 | 450 | 150.0 |
| 2024-01-04 | 180 | 630 | 176.7 |
| 2024-01-05 | 220 | 850 | 200.0 |
Running Total: UNBOUNDED PRECEDING AND CURRENT ROW means
"from the start of the partition to the current row"
Moving Average: 2 PRECEDING AND CURRENT ROW means
"current row plus 2 rows before it" (3-day average)
LAG() and LEAD() let you access values from previous or next rows:
| month | revenue | prev_month_revenue | next_month_revenue | change |
|---|---|---|---|---|
| 2024-01 | 10000 | NULL | 12000 | NULL |
| 2024-02 | 12000 | 10000 | 11500 | +2000 |
| 2024-03 | 11500 | 12000 | 13000 | -500 |
| 2024-04 | 13000 | 11500 | 14500 | +1500 |
| 2024-05 | 14500 | 13000 | NULL | +1500 |
Control exactly which rows are included in your window:
Physical offset based on row position
ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING
Includes: previous row, current row, next row
Logical offset based on value ranges
RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
All rows from start to current
Start of the partition
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
Entire partition
The current row being processed
ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
Current row to end
ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING
Get top 3 products per category
Assign percentile ranks to salaries
Find gaps in time series data
Calculate 7-day moving average
Without ORDER BY in your window function, the order is unpredictable!
Always specify ORDER BY for ranking and cumulative functions.
ROWS counts physical rows. RANGE includes all rows with the same value.
Use ROWS for most cases unless you specifically need value-based windows.
Reuse window definitions to keep your query DRY (Don't Repeat Yourself)
Head over to our SQL editor and try these window functions yourself!
Launch SQL Editor →