Problem
I’ve used SQL window functions for years, but I’ve never fully understood framing. To truly master window functions, you need at least a basic grasp of framing and how to apply it to your queries. Framing is especially helpful when working with aggregate functions. How can you overcome this hesitation to try framing?
Solution
In this article, we’ll explore the concept of framing in window functions. We’ll compare the differences between the ROWS and RANGE clauses and discuss when to choose one over the other. We’ll also highlight common pitfalls of framing and whether it applies to all types of window functions. By the end, you’ll better understand how framing works with window functions, making it seem less complex.
Inside the Window Frame
Microsoft introduced framing in SQL Server 2012. Before that, you could use aggregates with OVER, but not with an ORDER BY or frame. That’s right, if you wanted a running total or moving average, you had to find another way. So, what exactly is framing? It’s a more precise way to control which rows you focus on within a partition.
Framing allows you to make T-SQL behave more like a procedural language without needing a cursor or WHILE loop.
Now, let’s look at the elements that comprise framing.
Framing Elements
In this section, we’ll examine each part that brings framing to life. Since framing mainly relates to aggregate and analytic functions, the examples I’ll share will focus on the aggregate type.
To get started, let’s create a simple table named PhotoFrames with five columns, and ten rows. Use the code below to follow along.
/* MSSQLTips.com */
DROP TABLE IF EXISTS dbo.PhotoFrames;
GO
CREATE TABLE dbo.PhotoFrames
(
Id INT IDENTITY(1, 1) PRIMARY KEY CLUSTERED,
SalesDate DATE NOT NULL,
FrameStyle NVARCHAR(50),
SalesPerson NVARCHAR(50),
Amount DECIMAL(10, 2)
);
GO
INSERT INTO dbo.PhotoFrames
(
SalesDate,
FrameStyle,
SalesPerson,
Amount
)
VALUES
('2025-01-01', 'Rustic', 'Alice', 120.00),
('2025-01-01', 'Modern', 'Ben', 80.00),
('2025-01-02', 'Classic', 'Alice', 150.00),
('2025-01-02', 'Rustic', 'Ben', 200.00),
('2025-01-03', 'Modern', 'Cara', 90.00),
('2025-01-03', 'Classic', 'Dan', 220.00),
('2025-01-04', 'Rustic', 'Alice', 60.00),
('2025-01-05', 'Modern', 'Ben', 250.00),
('2025-01-05', 'Classic', 'Cara', NULL),
('2025-01-06', 'Rustic', 'Dan', 130.00);
GOThe code snippet below illustrates the basic structure of a frame for a running total.
/* MSSQLTips.com */
SELECT
SalesPerson,
SalesDate,
Amount,
SUM(Amount) OVER (ORDER BY
SalesDate ASC
RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS RunningTotal
FROM
dbo.PhotoFrames
ORDER BY
SalesDate ASC;
GO
First, we have the window function you plan to use, SUM in this case. Next is the OVER clause, which can turn almost any aggregate function into a window function. Inside the OVER clause, you can optionally define a PARTITION BY, as I don’t in the example above, and then an ORDER BY. When you use a frame, you must include an ORDER BY; if you forget it, SQL throws an error.
We could stop there and never worry about the frame, which is why many people never explore it; they don’t need to. But if we did, I wouldn’t have written this article.
Next, let’s explore the two arguments in the OVER clause after ORDER BY that set the frame in motion.
RANGE and ROWS
Microsoft defines that the ROWS and RANGE clauses are used to limit the rows within a partition by defining start and end points. When you define an ORDER BY, SQL uses the default frame unit of RANGE, meaning that if you do not specify a frame, one is automatically used. The default frame appears as shown in the code below.
RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROWRANGE focuses on the value of the row relative to the column specified in the ORDER BY clause. This is well demonstrated by the running total example, which clearly shows the difference. When using RANGE for a running total, rows with the same date (such as SalesDate in our example) are grouped based on their values.

ROWS, on the other hand, emphasizes the physical row position within the partition rather than the value.
Some people dismiss RANGE for running totals, but not so fast. Consider a situation where you want to see daily transactions but prefer not to define a unique ORDER BY and are satisfied with only the daily total. This is a case where RANGE excels.
With ROWS and RANGE out of the way, let’s take a look at how to set boundaries.
Frame Boundaries
I don’t believe these are referred to as boundaries in the official Microsoft documentation. However, I think that’s a good way to categorize them because that’s precisely what they do: establish a boundary from the start to the end of the frame. In other words, they define the start and stop points for the partition.
Below are the five boundary settings:
- CURRENT ROW: As the name implies, the current row.
- UNBOUNDED PRECEDING: The first row in the partition.
- UNBOUNDED FOLLOWING: The last row in the partition.
- n PRECEDING: n rows before the current row.
- n FOLLOWING: n rows after the current row.
The (n) in the examples above represents a numeric value. One important note is that RANGE does not support the numeric PRECEDING/FOLLOWING bounds.
Now, let’s apply all the information we just covered and look at a few simple examples. Try not to look up at the previous sections to see how much you remember.
Real-World Examples
What if we wanted to look at the current row and sum it to the previous row in the partition? Maybe we want to see the sum of sales for yesterday and today. Think about it for a minute before moving on.
Did you give it some thought? I hope so, and the code below should provide us with the correct results.
/* MSSQLTips.com */
SELECT
SalesPerson,
SalesDate,
Amount,
SUM(Amount) OVER (ORDER BY
SalesDate ASC
ROWS BETWEEN 1 PRECEDING AND CURRENT ROW
) AS RunningTotal
FROM
dbo.PhotoFrames
ORDER BY
SalesDate ASC;
GOBelow we can see if we add the current Amount with the previous row’s Amount we get the RunningTotal.

Let’s do one more. What if you want to see a column with the sum of an entire partition’s sales? For this example, let’s partition by the SalesPerson column. Give it your best try before looking at the answer below.
/* MSSQLTips.com */
SELECT
SalesDate,
SalesPerson,
Amount,
SUM(Amount) OVER (PARTITION BY
SalesPerson
ORDER BY
SalesDate ASC
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
) AS SumBySalesPerson
FROM
dbo.PhotoFrames
ORDER BY
SalesPerson,
SalesDate;
GOIn this example, if we sum the Amount for each person we get the same SumBySalesPerson amount for each of their rows.

Life Before Framing
The ranking window functions (ROW_NUMBER, RANK, DENSE_RANK, and NTILE) were introduced in SQL Server 2005. However, you couldn’t use ORDER BY (i.e., framing) in the OVER clause with an aggregate until SQL Server 2012. So, what did life look like before framing was introduced? Using the running total example, you would have needed a correlated subquery to return something like a running total or moving average.
I’m not saying that using a correlated subquery is a bad choice. There are definitely situations where it makes sense, but they don’t scale well when you want to add another type of frame to your SELECT clause. I’m glad that window functions were introduced, as they provide a simpler way to do this.
Additionally, an OUTER APPLY would have worked.
Pitfalls of Framing
Most great things in life have some downsides. There must surely be a few pitfalls when it comes to using framing. I can think of three. If you try to use one for a ranking function, SQL throws an error message.
Msg 10752, Level 15, State 3, Line 105
The function 'ROW_NUMBER' may not have a window frame.Now I can’t really think of a reason where you would actually use framing in a ranking function, but I’m sure someone out there has a use case.
The other issue which may crop up is performance when you extend you’re window over a million or even billions of rows. I imagine that SQL keeping the frame in context is draining resources when the frame could expand over millions of rows. Or even if you have thousands of partitions.
I don’t know if I would call it a pitfall, but sometimes people think that framing actually changes the partition, which it doesn’t. For example, no matter which rows you’re looking at, defined by the BETWEEN, they are still viewed only within the current partition.
Key Points
- If you plan on using window functions to their fullest potential, you must have at least a basic understanding of framing, which this article provides.
- The default partition frame is RANGE, but for aggregate and analytical functions, I find myself using ROWS more often.
- Remember for tie breaking that the ORDER BY clause should be deterministic if you want consistent results.
Next Steps
- Would you like to see an example that focuses on building a moving average with T-SQL? I wrote the article “Calculate a Moving Average with T-SQL Windowing Functions” to get you started.
- My favorite and most used windowing function is ROW_NUMBER. To learn more about it, read “SQL Server ROW_NUMBER for Ranking Rows.”
- Kathi Kellenberger hosted an excellent YouTube video for GroupBy. She discusses all the performance benefits of using windowing functions, especially improvements with SQL Server 2019.
- For a detailed overview of moving averages in SQL Server, see Rick Dobson’s article, “Weighted vs Simple Moving Average with SQL Server T-SQL Code.”

Jared Westover is a SQL Server specialist with two decades of industry experience covering T-SQL development, performance tuning, administration and Microsoft Fabric. He is currently a software architect at Crowe, an author at Pluralsight and primary contributor at sqlhabits.com. On MSSQLTips.com, Jared is a respected award-winning author for his clever T-SQL solutions and bringing to light new real-world solutions to age-old development problems.
- MSSQLTips Awards
- Achiever Award (75+ tips) – 2026
- Author of the Year – 2023
- Author Contender – 2024/2025


