Skip to content

Running Totals and Moving Averages in SQL Server

Window FRAME = which rows around the current row are included:

ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW   -> running total
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW           -> 3-row moving window
ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING           -> centered window
ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING   -> "remaining" total

ROWS = physical rows.

RANGE = rows with the same ORDER BY value are treated as one (ties grouped). DEFAULT frame when ORDER BY is present = RANGE UNBOUNDED PRECEDING (slower, tie surprises!)

Run the sample database script first. Every query below was run on that data, and the Output under each query is the real result.

USE SqlLearningDb;
GO

Topic table: BankTransactions

DROP TABLE IF EXISTS dbo.BankTransactions;

CREATE TABLE dbo.BankTransactions
(
    TxnID     INT IDENTITY(1,1) PRIMARY KEY,
    AccountNo VARCHAR(10)   NOT NULL,
    TxnDate   DATE          NOT NULL,
    TxnType   VARCHAR(10)   NOT NULL CHECK (TxnType IN ('Credit', 'Debit')),
    Amount    DECIMAL(12,2) NOT NULL,
    Remarks   VARCHAR(50)   NULL
);

INSERT INTO dbo.BankTransactions (AccountNo, TxnDate, TxnType, Amount, Remarks) VALUES
('ACC1001', '2025-01-01', 'Credit', 50000, 'Opening deposit'),
('ACC1001', '2025-01-05', 'Debit',   5000, 'ATM'),
('ACC1001', '2025-01-05', 'Debit',   1200, 'Groceries'),          -- same date (tie)
('ACC1001', '2025-01-10', 'Debit',  15000, 'Rent'),
('ACC1001', '2025-01-31', 'Credit', 60000, 'Salary'),
('ACC1001', '2025-02-03', 'Debit',  20000, 'EMI'),
('ACC1001', '2025-02-14', 'Debit',   3500, 'Dinner'),
('ACC1001', '2025-02-28', 'Credit', 60000, 'Salary'),
('ACC2002', '2025-01-02', 'Credit', 20000, 'Opening deposit'),
('ACC2002', '2025-01-15', 'Debit',  12000, 'Laptop EMI'),
('ACC2002', '2025-01-20', 'Debit',   9000, 'Travel'),             -- goes negative!
('ACC2002', '2025-01-31', 'Credit', 35000, 'Salary'),
('ACC2002', '2025-02-10', 'Debit',   4000, 'Shopping'),
('ACC2002', '2025-02-28', 'Credit', 35000, 'Salary');
GO
Messages
(14 rows affected)

Part A: Running Totals

Running balance per account (credit = +, debit = -)

SELECT AccountNo, TxnDate, TxnType, Amount,
       SUM(CASE WHEN TxnType = 'Credit' THEN Amount ELSE -Amount END)
           OVER (PARTITION BY AccountNo ORDER BY TxnDate, TxnID
                 ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS RunningBalance
FROM dbo.BankTransactions
ORDER BY AccountNo, TxnDate, TxnID;
Output
AccountNoTxnDateTxnTypeAmountRunningBalance
ACC10012025-01-01Credit50000.0050000.00
ACC10012025-01-05Debit5000.0045000.00
ACC10012025-01-05Debit1200.0043800.00
ACC10012025-01-10Debit15000.0028800.00
ACC10012025-01-31Credit60000.0088800.00
ACC10012025-02-03Debit20000.0068800.00
ACC10012025-02-14Debit3500.0065300.00
ACC10012025-02-28Credit60000.00125300.00
ACC20022025-01-02Credit20000.0020000.00
ACC20022025-01-15Debit12000.008000.00
ACC20022025-01-20Debit9000.00-1000.00
ACC20022025-01-31Credit35000.0034000.00
ACC20022025-02-10Debit4000.0030000.00
ACC20022025-02-28Credit35000.0065000.00

Find the moments when an account balance went Negative

WITH b AS
(
    SELECT AccountNo, TxnDate, Remarks,
           SUM(CASE WHEN TxnType = 'Credit' THEN Amount ELSE -Amount END)
               OVER (PARTITION BY AccountNo ORDER BY TxnDate, TxnID ROWS UNBOUNDED PRECEDING) AS Balance
    FROM dbo.BankTransactions
)
SELECT * FROM b WHERE Balance < 0;
Output
AccountNoTxnDateRemarksBalance
ACC20022025-01-20Travel-1000.00

Rows vs Range: look at 2025-01-05 (two rows with the same date)

SELECT AccountNo, TxnDate, Amount,
       SUM(Amount) OVER (PARTITION BY AccountNo ORDER BY TxnDate ROWS  UNBOUNDED PRECEDING) AS RunningRows,
       SUM(Amount) OVER (PARTITION BY AccountNo ORDER BY TxnDate RANGE UNBOUNDED PRECEDING) AS RunningRange,
       SUM(Amount) OVER (PARTITION BY AccountNo ORDER BY TxnDate)                           AS RunningDefault  -- = RANGE
FROM dbo.BankTransactions
WHERE AccountNo = 'ACC1001' AND TxnType = 'Debit';
Output
AccountNoTxnDateAmountRunningRowsRunningRangeRunningDefault
ACC10012025-01-055000.005000.006200.006200.00
ACC10012025-01-051200.006200.006200.006200.00
ACC10012025-01-1015000.0021200.0021200.0021200.00
ACC10012025-02-0320000.0041200.0041200.0041200.00
ACC10012025-02-143500.0044700.0044700.0044700.00

Year-to-date (ytd) and month-to-date (mtd) sales

WITH Daily AS
(
    SELECT SaleDate, SUM(TotalAmount) AS DayRevenue FROM dbo.Sales GROUP BY SaleDate
)
SELECT SaleDate, DayRevenue,
       SUM(DayRevenue) OVER (PARTITION BY YEAR(SaleDate)                  ORDER BY SaleDate ROWS UNBOUNDED PRECEDING) AS YTD,
       SUM(DayRevenue) OVER (PARTITION BY YEAR(SaleDate), MONTH(SaleDate) ORDER BY SaleDate ROWS UNBOUNDED PRECEDING) AS MTD
FROM Daily
ORDER BY SaleDate;
Output
SaleDateDayRevenueYTDMTD
2025-01-02245400.00245400.00245400.00
2025-01-041800.00247200.00247200.00
2025-01-0536000.00283200.00283200.00
2025-01-06261000.00544200.00544200.00
2025-01-0785000.00629200.00629200.00
2025-01-102250.00631450.00631450.00
2025-01-116300.00637750.00637750.00
2025-01-13305500.00943250.00943250.00
2025-01-14165000.001108250.001108250.00
2025-01-15248400.001356650.001356650.00
2025-01-1630000.001386650.001386650.00
2025-01-17140000.001526650.001526650.00
2025-01-1990000.001616650.001616650.00
2025-01-22195000.001811650.001811650.00
2025-01-23130000.001941650.001941650.00
... showing 15 of 135 rows

Running count of orders and running customer count per year

SELECT OrderID, OrderDate, CustomerID,
       COUNT(*) OVER (PARTITION BY YEAR(OrderDate) ORDER BY OrderDate, OrderID ROWS UNBOUNDED PRECEDING) AS OrdersSoFarThisYear
FROM dbo.Orders
ORDER BY OrderDate, OrderID;
Output
OrderIDOrderDateCustomerIDOrdersSoFarThisYear
12024-01-0911
22024-01-1782
32024-01-2683
42024-01-29134
52024-02-0635
62024-02-14156
72024-02-2047
82024-02-2778
92024-03-0639
102024-03-12110
112024-03-171711
122024-03-251612
132024-04-02113
142024-04-05214
152024-04-14315
... showing 15 of 80 rows

Part B: Moving Averages

3-day moving average of stock prices

SELECT Symbol, TradeDate, ClosePrice,
       CAST(AVG(ClosePrice) OVER (PARTITION BY Symbol ORDER BY TradeDate
                                  ROWS BETWEEN 2 PRECEDING AND CURRENT ROW) AS DECIMAL(10,2)) AS MA3,
       CAST(AVG(ClosePrice) OVER (PARTITION BY Symbol ORDER BY TradeDate
                                  ROWS BETWEEN 4 PRECEDING AND CURRENT ROW) AS DECIMAL(10,2)) AS MA5,
       COUNT(*)             OVER (PARTITION BY Symbol ORDER BY TradeDate
                                  ROWS BETWEEN 2 PRECEDING AND CURRENT ROW) AS RowsInWindow
FROM dbo.StockPrices
ORDER BY Symbol, TradeDate;
-- First rows have fewer than 3 values. Use RowsInWindow to hide incomplete averages if needed.
Output
SymbolTradeDateClosePriceMA3MA5RowsInWindow
INFY2025-06-021520.501520.501520.501
INFY2025-06-031534.201527.351527.352
INFY2025-06-041528.001527.571527.573
INFY2025-06-051550.751537.651533.363
INFY2025-06-061561.101546.621538.913
INFY2025-06-091549.901553.921544.793
INFY2025-06-101572.301561.101552.413
INFY2025-06-111580.001567.401562.813
TCS2025-06-023410.003410.003410.001
TCS2025-06-033398.403404.203404.202
TCS2025-06-043425.603411.333411.333
TCS2025-06-053440.103421.373418.533
TCS2025-06-063431.003432.233421.023
TCS2025-06-093460.803443.973431.183
TCS2025-06-103455.253449.023442.553
... showing 15 of 24 rows

7-day moving average of daily sales (fill missing days first with a calendar)

WITH Calendar AS
(
    SELECT CAST('2025-01-01' AS DATE) AS d
    UNION ALL SELECT DATEADD(DAY, 1, d) FROM Calendar WHERE d < '2025-06-30'
),
Daily AS
(
    SELECT c.d AS SaleDate, ISNULL(SUM(s.TotalAmount), 0) AS DayRevenue
    FROM Calendar c LEFT JOIN dbo.Sales s ON s.SaleDate = c.d
    GROUP BY c.d
)
SELECT SaleDate, DayRevenue,
       CAST(AVG(DayRevenue) OVER (ORDER BY SaleDate ROWS BETWEEN 6 PRECEDING AND CURRENT ROW) AS DECIMAL(12,2)) AS MovingAvg7Day,
       SUM(DayRevenue)      OVER (ORDER BY SaleDate ROWS BETWEEN 6 PRECEDING AND CURRENT ROW)                    AS Rolling7DayTotal
FROM Daily
ORDER BY SaleDate
OPTION (MAXRECURSION 200);
Output
SaleDateDayRevenueMovingAvg7DayRolling7DayTotal
2025-01-010.000.000.00
2025-01-02245400.00122700.00245400.00
2025-01-030.0081800.00245400.00
2025-01-041800.0061800.00247200.00
2025-01-0536000.0056640.00283200.00
2025-01-06261000.0090700.00544200.00
2025-01-0785000.0089885.71629200.00
2025-01-080.0089885.71629200.00
2025-01-090.0054828.57383800.00
2025-01-102250.0055150.00386050.00
2025-01-116300.0055792.86390550.00
2025-01-120.0050650.00354550.00
2025-01-13305500.0057007.14399050.00
2025-01-14165000.0068435.71479050.00
2025-01-15248400.00103921.43727450.00
... showing 15 of 181 rows

Centered moving average (1 before, current, 1 after)

SELECT Symbol, TradeDate, ClosePrice,
       CAST(AVG(ClosePrice) OVER (PARTITION BY Symbol ORDER BY TradeDate
                                  ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING) AS DECIMAL(10,2)) AS CenteredMA3
FROM dbo.StockPrices
WHERE Symbol = 'INFY';
Output
SymbolTradeDateClosePriceCenteredMA3
INFY2025-06-021520.501527.35
INFY2025-06-031534.201527.57
INFY2025-06-041528.001537.65
INFY2025-06-051550.751546.62
INFY2025-06-061561.101553.92
INFY2025-06-091549.901561.10
INFY2025-06-101572.301567.40
INFY2025-06-111580.001576.15

Rolling 3-month revenue (monthly level)

WITH Monthly AS
(
    SELECT DATEFROMPARTS(YEAR(o.OrderDate), MONTH(o.OrderDate), 1) AS OrderMonth,
           SUM(oi.LineTotal) AS Revenue
    FROM dbo.Orders o JOIN dbo.OrderItems oi ON oi.OrderID = o.OrderID
    GROUP BY DATEFROMPARTS(YEAR(o.OrderDate), MONTH(o.OrderDate), 1)
)
SELECT OrderMonth, Revenue,
       SUM(Revenue) OVER (ORDER BY OrderMonth ROWS BETWEEN 2 PRECEDING AND CURRENT ROW) AS Rolling3MonthRevenue,
       AVG(Revenue) OVER (ORDER BY OrderMonth ROWS BETWEEN 2 PRECEDING AND CURRENT ROW) AS Rolling3MonthAvg
FROM Monthly
ORDER BY OrderMonth;
Output
OrderMonthRevenueRolling3MonthRevenueRolling3MonthAvg
2024-01-01851410.00851410.00851410.000000
2024-02-01757610.001609020.00804510.000000
2024-03-01885825.002494845.00831615.000000
2024-04-01903466.502546901.50848967.166666
2024-05-01895150.002684441.50894813.833333
2024-06-01953160.002751776.50917258.833333
2024-07-01808200.002656510.00885503.333333
2024-08-01709800.002471160.00823720.000000
2024-09-01508075.002026075.00675358.333333
2024-10-01584910.001802785.00600928.333333
2024-11-01446850.001539835.00513278.333333
2024-12-01388775.001420535.00473511.666666
2025-01-01757675.001593300.00531100.000000
2025-02-01373550.001520000.00506666.666666
2025-03-01655075.001786300.00595433.333333
... showing 15 of 18 rows

Part C: Other Frame Tricks

Rolling MIN / MAX: 5-day high and low

SELECT Symbol, TradeDate, ClosePrice,
       MAX(ClosePrice) OVER (PARTITION BY Symbol ORDER BY TradeDate ROWS BETWEEN 4 PRECEDING AND CURRENT ROW) AS High5Day,
       MIN(ClosePrice) OVER (PARTITION BY Symbol ORDER BY TradeDate ROWS BETWEEN 4 PRECEDING AND CURRENT ROW) AS Low5Day
FROM dbo.StockPrices
ORDER BY Symbol, TradeDate;
Output
SymbolTradeDateClosePriceHigh5DayLow5Day
INFY2025-06-021520.501520.501520.50
INFY2025-06-031534.201534.201520.50
INFY2025-06-041528.001534.201520.50
INFY2025-06-051550.751550.751520.50
INFY2025-06-061561.101561.101520.50
INFY2025-06-091549.901561.101528.00
INFY2025-06-101572.301572.301528.00
INFY2025-06-111580.001580.001549.90
TCS2025-06-023410.003410.003410.00
TCS2025-06-033398.403410.003398.40
TCS2025-06-043425.603425.603398.40
TCS2025-06-053440.103440.103398.40
TCS2025-06-063431.003440.103398.40
TCS2025-06-093460.803460.803398.40
TCS2025-06-103455.253460.803425.60
... showing 15 of 24 rows

Remaining total: how much stock value is still "ahead"

SELECT ProductName, UnitPrice * StockQty AS StockValue,
       SUM(UnitPrice * StockQty) OVER (ORDER BY UnitPrice * StockQty DESC
                                       ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) AS RemainingValue
FROM dbo.Products;
Output
ProductNameStockValueRemainingValue
Old Model Tablet0.000.00
Ball Pen (Pack of 10)48000.0048000.00
Desk Organizer67500.00115500.00
A4 Notebook (Pack of 5)75000.00190500.00
Laser Printer80000.00270500.00
Wireless Mouse240000.00510500.00
USB-C Hub280000.00790500.00
Mechanical Keyboard300000.001090500.00
Standing Desk396000.001486500.00
Ergonomic Office Chair420000.001906500.00
Basic Office Chair425000.002331500.00
Lenovo ThinkCentre440000.002771500.00
Acer Aspire Desktop540000.003311500.00
27 inch Monitor540000.003851500.00
Redmi Note 13900000.004751500.00
... showing 15 of 20 rows

Pareto / 80-20 analysis: which products make 80% of revenue?

WITH ProdRev AS
(
    SELECT p.ProductName, SUM(oi.LineTotal) AS Revenue
    FROM dbo.OrderItems oi JOIN dbo.Products p ON p.ProductID = oi.ProductID
    GROUP BY p.ProductName
),
Cume AS
(
    SELECT ProductName, Revenue,
           SUM(Revenue) OVER (ORDER BY Revenue DESC ROWS UNBOUNDED PRECEDING) * 100.0
               / SUM(Revenue) OVER () AS CumulativePct
    FROM ProdRev
)
SELECT ProductName, Revenue, CAST(CumulativePct AS DECIMAL(5,2)) AS CumulativePct,
       CASE WHEN CumulativePct <= 80 THEN 'A (top 80%)' ELSE 'B/C' END AS ParetoClass
FROM Cume
ORDER BY Revenue DESC;
Output
ProductNameRevenueCumulativePctParetoClass
MacBook Air M33408000.0027.41A (top 80%)
HP Pavilion 141938000.0043.00A (top 80%)
iPhone 151774500.0057.27A (top 80%)
Lenovo ThinkCentre1438250.0068.84A (top 80%)
Acer Aspire Desktop915750.0076.21A (top 80%)
Dell Inspiron 15828750.0082.87B/C
27 inch Monitor510300.0086.98B/C
Samsung Galaxy A55355000.0089.83B/C
Redmi Note 13337500.0092.55B/C
Standing Desk325600.0095.16B/C
Ergonomic Office Chair201000.0096.78B/C
Basic Office Chair171700.0098.16B/C
Wireless Mouse84660.0098.84B/C
Mechanical Keyboard69250.0099.40B/C
USB-C Hub47075.0099.78B/C
... showing 15 of 18 rows

Practice Questions

  1. Show running total of 2025 sales per Region, by date.
  2. Show a 3-order moving average of order value for each customer.
  3. Show each account's month-end balance (hint: running total + last row per month).
  4. Find the first date when cumulative 2025 sales crossed 50 lakh (5,000,000).
  5. Compute a 7-day rolling count of orders.