An introduction to windowed functions

Windowed functions are a powerful feature of T-SQL, allowing you to perform advanced aggregates. They provide a very efficient way of doing this as soon as you just get the hang of the OVER() clause.


The common denominator for every windowed function i T-SQL is the OVER() clause. OVER() is added to any aggregate function that you want to turn into a windowed function, as well as those that are always windowed functions. The OVER() clause can contain PARTITION BY and ORDER BY or both, depending on the function.

You can use OVER() on all aggregate functions except GROUPING(), GROUPING_ID() and COUNT(DISTINCT).

PARTITION BY, and why it’s not the same as GROUP BY

Both standard aggregate functions and windowed aggregates using the OVER() clause perform aggregations. The main difference, however, is that whereas a standard aggregation uses a GROUP BY clause that is common to all of the aggregations, PARTITION BY allows you to define the GROUP BY expression for each aggregate function separately.

The following example will aggregate the minimum and maximum values for each currency code using aggregate functions and a GROUP BY:

SELECT ToCurrencyCode,
    MIN(EndOfDayRate) AS min_Rate,
    MAX(EndOfDayRate) AS max_Rate
FROM Sales.CurrencyRate
GROUP BY ToCurrencyCode;

Now, if you add the date column to the GROUP BY, this would be the equivalent of removing GROUP BY alltogether, because the GROUP BY would then encompass the entire primary key of the table. This effectively eliminates the MIN() and MAX() functions – those columns will always show each day’s rate:

SELECT CurrencyRateDate, ToCurrencyCode,
    MIN(EndOfDayRate) AS min_EODRate,
    MAX(EndOfDayRate) AS max_EODRate
FROM Sales.CurrencyRate
GROUP BY CurrencyRateDate, ToCurrencyCode;

So the problem is that you can’t get each day’s rates and the minimum and maximum rates over time in the same recordset without messing with subqueries and potentially killing performance. So here’s where we’ll use OVER() to define the grouping columns for each individual aggregate column:

SELECT CurrencyRateDate, ToCurrencyCode,
    EndOfDayRate AS EODRate,
    MIN(EndOfDayRate) OVER (
        PARTITION BY ToCurrencyCode) AS min_EODRate,
    MAX(EndOfDayRate) OVER (
        PARTITION BY ToCurrencyCode) AS max_EODRate
FROM Sales.CurrencyRate;

So the EODRate column is row-for-row the daily rate from Sales.CurrencyRate, without any aggregate (note that we’ve removed the GROUP BY!)

The MIN() and MAX() columns are calculated as windowed aggregates over all rows with the same FromCurrencyCode and ToCurrencyCode.

In conclusion: GROUP BY applies to all columns and requires all columns to either be in an aggregate function or in the GROUP BY clause. PARTITION BY defines an individual “GROUP BY” for each function.


There are a number of ranking and ordering functions available as windowed functions. This means that they also use the OVER() clause, but in addition to the (optional) PARTITION BY keyword, they also work with ORDER BY, still within the OVER() clause. Here’s how they work:

  • ROW_NUMBER() is the simplest one, returning a running row number, starting at 1 and incrementing by 1 for each row. If you use PARTITION BY, the counter will start over at 1 for each “partition”.
  • RANK() works like ROW_NUMBER(), except it employs a ranking function similar to what you would see in sporting results, with “shared” ranks.
  • DENSE_RANK() works like RANK(), but distincts the values before ranking them.

The following example will demonstrate how the three work:

FROM dbo.dummyTable


If you have a DISTINCT result set with windowed functions, you need to take into consideration the effect of how the logical processing order works. As you may recall, SELECT is performed before DISTINCT, which means that the windowed function will be calculated before the result set it DISTINCT’ed. This means that the following statement will return the same result sets whether you use DISTINCT or not:

FROM dbo.dummyTable

The reason is, of course, that the ROW_NUMBER() column is calculated first, and because it contains a unique number on every row, there are no duplicate rows to remove using the DISTINCT operator.

DISTINCT ROW_NUMBER()However, the RANK() and DENSE_RANK() functions return unique values for each x. This makes them better suited for use with a DISTINCT keyword.

DISTINCT DENSE_RANK()In other words, if you want a row numbering function to work with a DISTINCT query, you’re better off using DENSE_RANK().

FROM dbo.dummyTable

SQL Server 2012: Sliding window functions with LEAD() and LAG()

As of SQL Server 2012, there are two new functions called LEAD() and LAG(), among other new, practical windowed functions. These functions return the “next” and “previous” rows respectively from the result set. But they will be the subject of another post entirely.

As usual, if you have any questions or ideas, let me know!

11 thoughts on “An introduction to windowed functions

  1. Pingback: Know your collation orders | Sunday morning T-SQL

  2. Pingback: An introduction to table types | Sunday morning T-SQL

  3. Pingback: Working with intervals | Sunday morning T-SQL

  4. Pingback: A function to calculate recurring dates | Sunday morning T-SQL

  5. Pingback: Creating a top-10 list and aggregating the remainder | Sunday morning T-SQL

  6. Pingback: Using TOP n WITH TIES | Sunday morning T-SQL

  7. Pingback: About the spool operator | Sunday morning T-SQL

  8. Pingback: Calculating median using the NTILE function « Sunday morning T-SQL

  9. Pingback: Joining two SCD2 tables « Sunday morning T-SQL

  10. Pingback: Human-readable ranges of integers or dates | Sunday morning T-SQL

  11. Pingback: Automatically guessing foreign key constraints |

Let me hear your thoughts!

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.