SQL Server Window Functions
Window functions entered SQL:2003 but SQL Server didn't ship a full implementation until 2012. The 2005 release got as far as the ranking quartet and stopped. For seven years, if you wanted LAG, LEAD, a running total, or a frame clause on Microsoft's engine, you wrote ugly correlated subqueries.
What this guide covers
Window functions entered SQL:2003 but SQL Server didn't ship a full implementation until 2012. 2005 got as far as the ranking quartet (ROW_NUMBER, RANK, DENSE_RANK, NTILE) and stopped. For seven years, if you wanted LAG, LEAD, a running total, or a frame clause on Microsoft's engine, you wrote correlated subqueries.
Ranking functions
| Function | Available | Notes |
|---|---|---|
| ROW_NUMBER() | All versions | Works the same as PostgreSQL. Requires ORDER BY. |
| RANK() | All versions | Gaps after ties. Identical to PostgreSQL. |
| DENSE_RANK() | All versions | No gaps after ties. Identical to PostgreSQL. |
| NTILE(n) | All versions | Splits rows into n groups. Same behavior across engines. |
Offset functions
| Function | Available | Notes |
|---|---|---|
| LAG() | SQL Server 2012+ | Access previous row. Same syntax as PostgreSQL. |
| LEAD() | SQL Server 2012+ | Access next row. Same syntax as PostgreSQL. |
| FIRST_VALUE() | SQL Server 2012+ | First value in the window frame. |
| LAST_VALUE() | SQL Server 2012+ | Last value in frame. Watch for default frame behavior. |
Aggregate window functions
| Function | Available | Notes |
|---|---|---|
| SUM() OVER() | All versions | Running totals. Requires frame clause for cumulative sums. |
| AVG() OVER() | All versions | Moving averages. Frame clause controls the window. |
| COUNT() OVER() | All versions | Running counts. Works with or without PARTITION BY. |
| MIN/MAX() OVER() | All versions | Running min/max within the window. |
SQL Server vs PostgreSQL: key differences
Specific areas where SQL Server window function behavior differs. Knowing these prevents syntax errors during interviews and on the job.
| Feature | PostgreSQL | SQL Server |
|---|---|---|
| FILTER Clause | Supported: SUM(amount) FILTER (WHERE status = 'paid') OVER (...) | Not supported. Use CASE WHEN inside the aggregate. |
| Frame Clause (ROWS/RANGE) | Full support since PostgreSQL 8.4. ROWS BETWEEN, RANGE BETWEEN, GROUPS BETWEEN (PG 11+). | ROWS BETWEEN since SQL Server 2012. RANGE has limited support. GROUPS not supported. Pre-2012 had no frame clauses. |
| NULLS FIRST / NULLS LAST | Supported: ORDER BY col ASC NULLS LAST. | Not supported as syntax. NULLs sort first for ASC and last for DESC by default. Workaround: ORDER BY CASE WHEN col IS NULL THEN 1 ELSE 0 END, col. |
| DISTINCT in window aggregates | Not supported. | Also not supported. Neither allows COUNT(DISTINCT col) OVER (). |
| STRING_AGG / LISTAGG with OVER | Cannot be used as window function with OVER. | STRING_AGG (2017+) also cannot be used with OVER. Neither engine supports windowed string concatenation. |
Running totals and moving averages
-- Running total in SQL Server (2012+)
SELECT
order_date,
daily_revenue,
SUM(daily_revenue) OVER (
ORDER BY order_date
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS running_total
FROM daily_sales;
-- Moving 7-day average
SELECT
order_date,
daily_revenue,
AVG(daily_revenue) OVER (
ORDER BY order_date
ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
) AS seven_day_avg
FROM daily_sales;Running totals are the most common window aggregate in interviews. SQL Server requires an explicit frame clause for cumulative sums. Without one, the default depends on whether ORDER BY is present.
Default frame gotcha with tied ORDER BY values
-- Default frame (RANGE) with ties: may surprise you
SELECT
order_date,
amount,
SUM(amount) OVER (ORDER BY order_date) AS running_sum
FROM orders;
-- If two rows have the same order_date,
-- both see the sum INCLUDING the other tied row.
-- Fix: specify ROWS explicitly
SELECT
order_date,
amount,
SUM(amount) OVER (
ORDER BY order_date
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS running_sum
FROM orders;
-- Each row sees only rows up to and including itselfBoth SQL Server and PostgreSQL default to RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW when ORDER BY is present. Tied rows are included in the frame together, producing unexpected running totals. Always specify ROWS BETWEEN explicitly.
3 SQL Server window function interview questions
Top 3 highest-paid employees per department (SQL Server).
Tests basic window function usage. Syntax is identical to PostgreSQL. Use ROW_NUMBER() OVER (PARTITION BY department ORDER BY salary DESC) in a CTE, then WHERE rn <= 3. If 'including ties,' switch to RANK or DENSE_RANK.
30-day rolling average of daily revenue (SQL Server).
Frame clause knowledge. AVG(daily_revenue) OVER (ORDER BY order_date ROWS BETWEEN 29 PRECEDING AND CURRENT ROW). ROWS is preferred over RANGE to avoid including tied dates. If pre-2012, use a self-join: join the table to itself where the joined date is within 30 days, then average.
What SQL Server window function features are missing vs PostgreSQL?
FILTER clause (not supported, use CASE WHEN). NULLS FIRST/LAST (use CASE WHEN in ORDER BY). GROUPS frame type (not supported). Limited RANGE before 2022. Ranking functions (ROW_NUMBER, RANK, DENSE_RANK) work identically, so most interview problems have the same solution regardless of engine.
SQL Server window functions FAQ
Does SQL Server support window functions?+
What window functions are missing in SQL Server vs PostgreSQL?+
How do you write a running total in SQL Server?+
Two decades of window functions, one interview
- 01
Active recall beats re-reading by 50%
Cognitive-science meta-reviews (Dunlosky et al., 2013) rank practice testing as a top-tier study technique, while re-reading and highlighting rank near the bottom
- 02
76% of hiring managers reject on the coding task, not the resume
From HackerRank's 2024 Developer Skills Report. Candidates who look strong on paper still fail the live screen if they haven't done timed, executable practice
- 03
Five problem shapes cover 80% of data engineer loops
Dedup, sessionization, top-N-per-group, slowly-changing dimensions, partition tricks. Writing the shapes by hand turns the unfamiliar into pattern recognition