Back to Blog

SQL Subquery

How to Calculate a SQL Moving Average without a Cursor

Calculate moving averages using window functions. Learn ROWS BETWEEN for rolling calculations and trend analysis in SQL Server.

5 min read

Last updated on

Update: If you are working with the newest versions of SQL Server, you can use the windowing functions to accomplish the same thing. I posted the updated code at the end of the post. For this video, I still like the thought process of anchoring to a date.

There are debates on the best way to do a SQL Moving Average in SQL Server.

Some people think there are times when a cursor is most efficient. Other-s think that you can do it all in a set-based way without the cursor.

The other day I was going to calculate a moving average and my first thought was to use a cursor.

I did some quick research and found this forum question: Moving Average in TSQL

There is a post that shows a subquery with an anchor date to help find the 1 and 2 day offset.

Here is the script you can use to test the 3 day SQL Moving Average final result.

CREATE TABLE [dbo].[daily_sales](

    [id] [int] NULL,

    [dt] [datetime] NULL,

    [revenue] [decimal](12, 2) NULL

) ON [PRIMARY]

GO

INSERT [dbo].[daily_sales] ([id], [dt], [revenue])

VALUES (1, CAST(0x0000A15700000000 AS DateTime), CAST(125.00 AS Decimal(12, 2)))

GO

INSERT [dbo].[daily_sales] ([id], [dt], [revenue])

VALUES (2, CAST(0x0000A15800000000 AS DateTime), CAST(114.00 AS Decimal(12, 2)))

GO

INSERT [dbo].[daily_sales] ([id], [dt], [revenue])

VALUES (3, CAST(0x0000A15900000000 AS DateTime), CAST(92.00 AS Decimal(12, 2)))

GO

INSERT [dbo].[daily_sales] ([id], [dt], [revenue])

VALUES (4, CAST(0x0000A15A00000000 AS DateTime), CAST(152.00 AS Decimal(12, 2)))

GO

INSERT [dbo].[daily_sales] ([id], [dt], [revenue])

VALUES (5, CAST(0x0000A15B00000000 AS DateTime), CAST(48.00 AS Decimal(12, 2)))

GO

Here is the final query.

SELECT

            DATEADD(DAY, days_since_1900, '19000101') AS dt,

            AVG(revenue) AS Revenue_3_day_moving_average

FROM (

            select

            DATEDIFF(DAY, '18991230', dt) AS days_since_1900

            ,revenue

            ,0 as Actualdate

            from daily_sales

            UNION ALL

            select

            DATEDIFF(DAY, '18991231', dt) AS days_since_1900

            ,revenue

            ,0 as Actualdate

            from daily_sales

            UNION ALL

            select

            DATEDIFF(DAY, '19000101', dt) AS days_since_1900

            ,revenue

            ,1 as Actualdate

            from daily_sales

) AS daily_sales_normalized_to_1900

GROUP BY    days_since_1900

HAVING        MAX(Actualdate) = 1

ORDER BY    dt

Here is the query you would use with SQL Server 2012.

select dt,

    avg(revenue)

    OVER (

        ORDER BY dt

        ROWS BETWEEN 2 PRECEDING AND CURRENT ROW

    )

FROM daily_sales

The original Youtube video: SQL Moving Average

About Joey Blue

Joey Blue teaches practical data skills that companies actually use. With 25+ years of experience solving real data problems for Fortune 500 companies, he's helped 152,000+ students learn SQL, Power BI, reporting, and modern analytics—cutting straight to what works.