Note ROW_NUMBER assigns a unique sequential number. Ties get arbitrary ordering - two rows with the same total_amount will get different numbers based on physical storage order. Use RANK or DENSE_RANK if you need tie handling.
Note RANK leaves gaps after ties (1, 2, 2, 4). DENSE_RANK does not (1, 2, 2, 3). Choose based on whether you need contiguous numbers or positional accuracy.
-- department_id | first_name | salary | dept_rank
-- 1 | Alice | 95000 | 1
-- 1 | Bob | 82000 | 2
-- 2 | Carol | 105000 | 1
-- 2 | Dave | 78000 | 2
Note PARTITION BY divides rows into groups and applies the window function independently within each group. It is like GROUP BY but without collapsing rows. You can partition by multiple columns.
partition bywindow per grouprank within grouptop n per category
Note LAG looks at previous rows; LEAD looks at subsequent rows. The optional second argument specifies how many rows to look back/ahead (default 1). The third argument provides a default value instead of NULL for the first/last row.
previous rownext rowlagleadcompare to previousrow before
SUM / AVG OVER (Running Totals)
Syntax
SUM(column)OVER(ORDERBYcolumn ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)
Example
SELECT
order_date,
total_amount,SUM(total_amount)OVER(ORDERBY order_date)AS running_total,AVG(total_amount)OVER(ORDERBY order_date
ROWS BETWEEN2 PRECEDING AND CURRENT ROW
)AS moving_avg_3
FROM orders
WHERE user_id =42;
Note Without ROWS/RANGE, the default frame is RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW, which includes all rows with the same ORDER BY value. Use ROWS for exact row-based windows. This distinction matters when ORDER BY has ties.
Note NTILE divides rows into n approximately equal groups. If the row count is not evenly divisible, earlier groups get one extra row. Useful for percentile bucketing, but the buckets are based on row count, not value distribution.
ntilequartilepercentile bucketdivide into groupsequal groups
FIRST_VALUE / LAST_VALUE
Syntax
FIRST_VALUE(column)OVER(ORDERBYcolumn)LAST_VALUE(column)OVER(ORDERBYcolumn ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING)
-- Shows each employee alongside their department's top earner
Note LAST_VALUE has a critical gotcha: the default window frame ends at CURRENT ROW, not at the end of the partition. You must add ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING to get the true last value.
first valuelast valuefirst in grouphighest in partition
Window Frame Specification
Syntax
ROWS BETWEEN start ANDend-- start/end: UNBOUNDED PRECEDING | n PRECEDING | CURRENT ROW | n FOLLOWING | UNBOUNDED FOLLOWING
Example
SELECT
sale_date,
amount,AVG(amount)OVER(ORDERBY sale_date
ROWS BETWEEN6 PRECEDING AND CURRENT ROW
)AS weekly_moving_avg,SUM(amount)OVER(ORDERBY sale_date
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
)AS cumulative_total
FROM daily_sales;
Output
-- 7-day moving average alongside cumulative total
Note ROWS counts physical rows. RANGE groups rows with the same ORDER BY value together. For date-based ranges in PostgreSQL, you can use RANGE BETWEEN INTERVAL '7 days' PRECEDING AND CURRENT ROW if the ORDER BY column is a date.
SELECT...FROMtableWINDOW w AS(PARTITIONBY col ORDERBY col);
Example
SELECT
department_id,
first_name,
salary,ROW_NUMBER()OVER w AS dept_rank,SUM(salary)OVER w AS running_salary,AVG(salary)OVER w AS running_avg
FROM employees
WINDOW w AS(PARTITIONBY department_id ORDERBY salary DESC);
Output
-- Reuses the same window definition for three different functions
Note The WINDOW clause avoids repeating the same OVER specification. It is ANSI SQL and supported in PostgreSQL and MySQL 8+. You can still modify a named window in individual OVER clauses to add frame specs.
SELECT
first_name,
salary,PERCENT_RANK()OVER(ORDERBY salary)AS pct_rank,CUME_DIST()OVER(ORDERBY salary)AS cumulative_dist
FROM employees;
Output
-- first_name | salary | pct_rank | cumulative_dist
-- Dave | 50000 | 0.00 | 0.25
-- Carol | 65000 | 0.33 | 0.50
-- Bob | 82000 | 0.67 | 0.75
-- Alice | 95000 | 1.00 | 1.00
Note PERCENT_RANK returns (rank - 1) / (total - 1), ranging from 0 to 1. CUME_DIST returns the fraction of rows with values less than or equal to the current row. Both are useful for percentile analysis.