【问题标题】:SQL Server : 5 days moving average for last monthSQL Server:上个月的 5 天移动平均值
【发布时间】:2014-02-22 03:59:33
【问题描述】:

我有两列 TOTAL 和 DATE 的视图,后者不包括周六和周日,即

TOTAL      DATE
 0         1-1-2014
33         2-1-2014
11         3-1-2014
55         5-1-2014
... 
25         15-1-2014
35         16-1-2014
17         17-1-2014
40         20-1-2014
33         21-1-2014
...

我要完成的任务是计算整个月的 5 天 TOTAL 平均值,即截至当前日期的 13 日和 17 日、14 日和 20 日(我们跳过周末)、15 日和 21 日等之间。 是的,它们是重叠的范围。

知道如何在 SQL 中实现它吗?

输出示例(从第 6 位开始并使用假数字)

 5daysAVG        Start_day
 22               1-01-2014   <-counted between 1st to 6th Jan excl 4 and 5 of Jan
 25               2-01-2014   <- Counted between 2nd to 7th excluding 4 and 5
 27               3-01-2014   <- 3rd to 8th excluding 4/5
 24               6-01-2014   <-6th to 10th
 ...
 33               today-5

【问题讨论】:

  • 为什么从 13 号开始?这个月从 1 号开始,对吗?此外,您的单圈范围似乎超出了范围。
  • 当然,这只是示例,是的,它们是重叠的
  • 所以让我们得到这个。您想要五人一组的平均值吗?
  • 你能提供一个你想要的结果的例子吗?

标签: sql sql-server sql-server-2008


【解决方案1】:

好的,我通常会设置一些测试数据来玩。

下面是一些在 tempdb 中创建 [work] 表的代码。我正在跳过周末。总数是一个从0到40的随机数。

-- Just playing
use tempdb;
go

-- drop existing
if object_id ('work') > 0
drop table work
go

-- create new
create table work
(
  my_date date,
  my_total int
);
go

-- clear data
truncate table work;
go

-- Monday = 1 
SET DATEFIRST 1;
GO

-- insert data
declare @dt date = '20131231';
declare @hr int;
while (@dt < '20140201')
begin
  set @hr = floor(rand(checksum(newid())) * 40);
  set @dt = dateadd(d, 1, @dt);
  if (datepart(dw, @dt) < 6)
    insert into work values (@dt, @hr);
end
go

这在 SQL SERVER 2012 中使用新的 LEAD() 窗口函数变得非常容易。

-- show data
with cte_summary as
(
select 
  row_number() over (order by my_date) as my_num,
  my_date, 
  my_total,
  LEAD(my_total, 0, 0) OVER (ORDER BY my_date) +
  LEAD(my_total, 1, 0) OVER (ORDER BY my_date) +
  LEAD(my_total, 2, 0) OVER (ORDER BY my_date) +
  LEAD(my_total, 3, 0) OVER (ORDER BY my_date) +
  LEAD(my_total, 4, 0) OVER (ORDER BY my_date) as my_sum,
  (select count(*) from work) as my_cnt
from work 
)
select * from cte_summary 
where my_num <= my_cnt - 4

基本上,我们为每一行指定一个行号,计算第 0 行(当前)到第 4 行(4 行之外)的总和以及总计数。

由于这是五个期间的运行总计,因此剩余日期缺少数据。因此,我们将它们扔掉。 my_row

希望这能解决你的问题!

如果您只关心一个月的一个数字,请将选择更改为以下。我留下了其他行,以便您了解正在发生的事情。

select avg(my_sum/5) as my_stat
from cte_summary 
where my_num <= my_cnt - 4

FOR SQL SERVER = 2005

就像这个世界上的任何事情一样,总有办法做到这一点。我使用了一个小计数表来循环遍历数据并收集 5 个数据点的集合作为平均值。

-- show data
with
cte_tally as
(
select 
    row_number() over (order by (select 1)) as n
from 
    sys.all_columns x 
),
cte_data as
(
select 
    row_number() over (order by my_date) as my_num,
    my_date, 
    my_total 
from 
    work 
)
select 
  (select my_date from cte_data where my_num = n) as the_date,
  (
    select sum(my_total) / 5 
    from cte_data 
    where my_num >= n and my_num < n+5
  ) as the_average
from cte_tally
where n <= (select count(*)-4 from work) 

以下是公用表表达式 (CTE) 的说明。

cte_data = order data by date and give row numbers
cte_tally = a set based counting algorithm

五人一组计算平均值并显示日期。

此解决方案不依赖于节假日或周末。如果有数据,它只是按日期按五组为一组进行分区。

如果您需要过滤掉假期和周末,请创建一个假期表。将 where 子句添加到 cte_data 以检查 NOT IN (SELECT DATE FROM HOLIDAY TABLE)。

祝你好运!

【讨论】:

  • 会的,但正如你所说的 LEAD 在 SQL Server 2012 中,我在 2008 R2 上工作:(
【解决方案2】:

SQL Server 提供了datepart(wk, ...) 函数来获取一年中的星期几。不幸的是,它使用一年的第一天来定义年份。

相反,您可以找到连续值的序列并将它们组合在一起:

select min(date), max(date, avg(total*1.0)
from (select v.*, row_number() over (order by date) as seqnum
      from view
     ) v
group by dateadd(day, -seqnum, date);

这个想法是从连续几天的序列中减去一个数字序列得到一个常数。

您也可以通过使用规范日期并除以 7 来做到这一点:

select min(date), max(date, avg(total*1.0)
from view v
group by datediff(day, '2000-01-03', date) / 7;

日期'2000-01-03' 是任意星期一。

编辑:

您似乎想要一个 5 天移动平均线。因为周末缺少数据,avg() 应该可以正常工作:

select v1.date, avg(v2.value)
from view v1 join
     view v2
     on v2.date >= v1.date and v2.date < dateadd(day, 7, v1.date)
group by v1.date;

【讨论】:

  • 效果很好!只是我无法理解 dateadd(day, 7, v1.date)。为什么是 7 而不是 5?
  • @KrisWojcik 。 . .也许我误解了正在发生的事情,但您似乎想要接下来 5 天的数据 - 而 7 天就会给您(因为周末的两天不见了)。
  • 哦,对了。忘记了 dateadd 我们不能简单地假设它会错过周末,d'uh...
  • 不解决五天滚动平均值。不考虑节假日。
【解决方案3】:

这是一个适用于 SQL 2008 的解决方案;

这里的概念是先用一个表变量来规范化数据;剩下的就是简单的数学计算和平均天数。

通过规范化数据,我的意思是,摆脱周末,并在可用于标识行的临时表变量中分配 ID;

查看:(SqlFiddle also here)

-- This represents your original source table
Declare @YourSourceTable Table 
(
    Total Int, 
    CreatedDate DateTime
)

-- This represents some test data in your table with 2 weekends
Insert Into @YourSourceTable Values (0, '1-1-2014')
Insert Into @YourSourceTable Values (33, '1-2-2014')
Insert Into @YourSourceTable Values (11, '1-3-2014')
Insert Into @YourSourceTable Values (55, '1-4-2014')
Insert Into @YourSourceTable Values (25, '1-5-2014')
Insert Into @YourSourceTable Values (35, '1-6-2014')
Insert Into @YourSourceTable Values (17, '1-7-2014')
Insert Into @YourSourceTable Values (40, '1-8-2014')
Insert Into @YourSourceTable Values (33, '1-9-2014')
Insert Into @YourSourceTable Values (43, '1-10-2014')
Insert Into @YourSourceTable Values (21, '1-11-2014')
Insert Into @YourSourceTable Values (5, '1-12-2014')
Insert Into @YourSourceTable Values (12, '1-13-2014')
Insert Into @YourSourceTable Values (16, '1-14-2014')

-- Just a quick test to see the source data
Select * From @YourSourceTable

/*  Now we need to normalize the data; 
    Let's just remove the weekends and get some consistent ID's to use in a separate table variable
    We will use DateName SQL Function to exclude weekend days while also giving 
    sequential ID's to the remaining data in our temporary table variable,
    which are easier to query later 
*/

Declare @WorkingTable Table 
(
    TempID Int Identity, 
    Total Int, 
    CreatedDate DateTime
)

-- Let's get the data normalized:
Insert Into 
    @WorkingTable
Select 
    Total,  
    CreatedDate
From @YourSourceTable
Where DateName(Weekday, CreatedDate) != 'Saturday' 
    And DateName(Weekday, CreatedDate) != 'Sunday' 

-- Let's run a 2nd quick sanity check to see our normalized data
Select * From @WorkingTable

/*  Now that data is normalized, we can just use the ID's to get each 5 day range and
    perform simple average function on the columns; I chose to use a CTE here just to
    be able to query it and drop the NULL ranges (where there wasn't 5 days of data)
    without having to recalculate each average
*/

; With rangeCte (StartDate, TotalAverage)
As
(
    Select
        wt.createddate As StartDate,
        (
            wt.Total +  
            (Select Total From @WorkingTable Where TempID = wt.TempID + 1) +
            (Select Total From @WorkingTable Where TempID = wt.TempID + 2) +
            (Select Total From @WorkingTable Where TempID = wt.TempID + 3) +
            (Select Total From @WorkingTable Where TempID = wt.TempID + 4)
        ) / 5
        As TotalAverage
From 
    @WorkingTable wt    
)
Select 
    StartDate,
    TotalAverage
From rangeCte
Where TotalAverage 
    Is Not Null

【讨论】:

  • 这适用于滚动的五天平均值;但是,您有多个 TSQL 语句。它也不会平均平均值。您如何计算最后 4 天没有完整的 5 天滚动?我把它们扔掉!
  • @CRAFTYDBA:多个语句没有错。顺便说一句,您的解决方案还使用了多个 TSQL 语句;不知道你为什么提出这个。如果您指的是我的额外 SELECT,它们仅用于插图/可视化,并被这样评论。不确定平均平均值是什么意思,我不认为这是一项要求 - 我的代码平均每个滚动 5 天忽略周末。最后 4 天也在我的代码中被抛出(如果您阅读我的 sql 中的 cmets,您会看到发生这种情况的位置,提示,它在从 CTE 中选择的查询中,其中 TotalAverage 不为空。
猜你喜欢
  • 1970-01-01
  • 2016-07-07
  • 2014-01-24
  • 1970-01-01
  • 1970-01-01
  • 2020-07-09
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多