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.

2003
ANSI adds window funcs
2005
SQL Server adds ranking
2012
LAG, LEAD, frame clause
2001
Oracle 8i, the original

Ranking functions

FunctionAvailableNotes
ROW_NUMBER()All versionsWorks the same as PostgreSQL. Requires ORDER BY.
RANK()All versionsGaps after ties. Identical to PostgreSQL.
DENSE_RANK()All versionsNo gaps after ties. Identical to PostgreSQL.
NTILE(n)All versionsSplits rows into n groups. Same behavior across engines.

Offset functions

FunctionAvailableNotes
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

FunctionAvailableNotes
SUM() OVER()All versionsRunning totals. Requires frame clause for cumulative sums.
AVG() OVER()All versionsMoving averages. Frame clause controls the window.
COUNT() OVER()All versionsRunning counts. Works with or without PARTITION BY.
MIN/MAX() OVER()All versionsRunning 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.

FeaturePostgreSQLSQL Server
FILTER ClauseSupported: 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 LASTSupported: 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 aggregatesNot supported.Also not supported. Neither allows COUNT(DISTINCT col) OVER ().
STRING_AGG / LISTAGG with OVERCannot 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 itself

Both 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?+
Yes. Ranking window functions (ROW_NUMBER, RANK, DENSE_RANK, NTILE) since SQL Server 2005. 2012 added LAG, LEAD, FIRST_VALUE, LAST_VALUE, and ROWS/RANGE frame clauses. 2022 improved RANGE support. For most interview questions, syntax is identical to PostgreSQL.
What window functions are missing in SQL Server vs PostgreSQL?+
FILTER clause for conditional window aggregates, NULLS FIRST/NULLS LAST syntax for ORDER BY, GROUPS frame type (PG 11+), more limited RANGE support. Workarounds: CASE WHEN for FILTER, CASE in ORDER BY for NULLS, ROWS instead of GROUPS or RANGE.
How do you write a running total in SQL Server?+
SUM with OVER and a ROWS frame: SUM(amount) OVER (ORDER BY date ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW). The ROWS keyword ensures each row sees only rows up to and including itself. Without ROWS, the default RANGE frame may include tied rows together. Requires SQL Server 2012+.
02 / Why practice

Two decades of window functions, one interview

  1. 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

  2. 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

  3. 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

Related guides