如何在SQL Server中通过滑动窗口聚合(计算不同的项)?

如何在SQL Server中通过滑动窗口聚合(计算不同的项)?,sql,sql-server,count,aggregate-functions,sliding-window,Sql,Sql Server,Count,Aggregate Functions,Sliding Window,我目前正在使用此查询(在SQL Server中)计算每天唯一项目的数量: SELECT Date, COUNT(DISTINCT item) FROM myTable GROUP BY Date ORDER BY Date Date count 01/01/2018 2 02/01/2018 1 03/01/2018 1 04/01/2018 1 如何将其转换为过去3天(包括当天)内每个日期的唯一项目数 输出应为具有2列的表: 一列包含原始表中的所

我目前正在使用此查询(在SQL Server中)计算每天唯一项目的数量:

SELECT Date, COUNT(DISTINCT item) 
FROM myTable 
GROUP BY Date 
ORDER BY Date
Date        count  
01/01/2018  2  
02/01/2018  1  
03/01/2018  1  
04/01/2018  1
如何将其转换为过去3天(包括当天)内每个日期的唯一项目数

输出应为具有2列的表: 一列包含原始表中的所有日期。在第二列中,我们有每个日期的唯一项目数

例如,如果原始表为:

Date        Item  
01/01/2018  A  
01/01/2018  B  
02/01/2018  C  
03/01/2018  C    
04/01/2018  C
通过上面的查询,我目前获得了每天的唯一计数:

SELECT Date, COUNT(DISTINCT item) 
FROM myTable 
GROUP BY Date 
ORDER BY Date
Date        count  
01/01/2018  2  
02/01/2018  1  
03/01/2018  1  
04/01/2018  1
我希望得到3天滚动窗口内的唯一计数结果:

Date        count  
01/01/2018  2  
02/01/2018  3  (because items ABC on 1st and 2nd Jan)
03/01/2018  3  (because items ABC on 1st,2nd,3rd Jan)    
04/01/2018  1  (because only item C on 2nd,3rd,4th Jan)    
使用
GETDATE()
函数获取当前日期,使用
DATEADD()
获取最后3天

 SELECT Date, count(DISTINCT item) 
 FROM myTable 
 WHERE [Date] >= DATEADD(day,-3, GETDATE())
 GROUP BY Date 
 ORDER BY Date

使用
apply
可以方便地形成滑动窗口

CREATE TABLE myTable 
    ([DateCol] datetime, [Item] varchar(1))
;

INSERT INTO myTable 
    ([DateCol], [Item])
VALUES
    ('2018-01-01 00:00:00', 'A'),
    ('2018-01-01 00:00:00', 'B'),
    ('2018-01-02 00:00:00', 'C'),
    ('2018-01-03 00:00:00', 'C'),
    ('2018-01-04 00:00:00', 'C')
;

CREATE NONCLUSTERED INDEX IX_DateCol  
    ON MyTable([Date])  
;    
查询

select distinct 
       t1.dateCol
     , oa.ItemCount
from myTable t1
outer apply (
      select count(distinct t2.item) as ItemCount
      from myTable t2
      where t2.DateCol between dateadd(day,-2,t1.DateCol) and t1.DateCol
  ) oa
order by t1.dateCol ASC
|              dateCol | ItemCount |
|----------------------|-----------|
| 2018-01-01T00:00:00Z |         2 |
| 2018-01-02T00:00:00Z |         3 |
| 2018-01-03T00:00:00Z |         3 |
| 2018-01-04T00:00:00Z |         1 |

select distinct 
       t1.dateCol
     , oa.ItemCount
from myTable t1
outer apply (
      select count(distinct t2.item) as ItemCount
      from myTable t2
      where t2.DateCol between dateadd(day,-2,t1.DateCol) and t1.DateCol
  ) oa
order by t1.dateCol ASC
|              dateCol | ItemCount |
|----------------------|-----------|
| 2018-01-01T00:00:00Z |         2 |
| 2018-01-02T00:00:00Z |         3 |
| 2018-01-03T00:00:00Z |         3 |
| 2018-01-04T00:00:00Z |         1 |
在使用
apply
之前,通过减少
date
列,可能会提高一些性能,如下所示:

select 
       d.date
     , oa.ItemCount
from (
    select distinct t1.date
    from myTable t1
     ) d
outer apply (
      select count(distinct t2.item) as ItemCount
      from myTable t2
      where t2.Date between dateadd(day,-2,d.Date) and d.Date
  ) oa
order by d.date ASC
;
在该子查询中,您可以使用
groupby
,而不是使用
selectdistinct
,但执行计划将保持不变


分组依据
应该比
不同
更快(确保在
日期
列上有索引)

SQL 演示
Rextester演示:

最直接的解决方案是根据日期将表与自身连接起来:

SELECT t1.DateCol, COUNT(DISTINCT t2.Item) AS C
FROM testdata AS t1 
LEFT JOIN testdata AS t2 ON t2.DateCol BETWEEN DATEADD(dd, -2, t1.DateCol) AND t1.DateCol
GROUP BY t1.DateCol
ORDER BY t1.DateCol
输出:

| DateCol                 | C |
|-------------------------|---|
| 2018-01-01 00:00:00.000 | 2 |
| 2018-01-02 00:00:00.000 | 3 |
| 2018-01-03 00:00:00.000 | 3 |
| 2018-01-04 00:00:00.000 | 1 |
|        Date             | Count |
|-------------------------|-------|
| 2018-01-01 00:00:00.000 |   2   |
| 2018-01-02 00:00:00.000 |   3   |
| 2018-01-03 00:00:00.000 |   3   |
| 2018-01-04 00:00:00.000 |   1   |

此解决方案与其他解决方案不同。您是否可以通过与其他答案的比较来检查此查询在真实数据上的性能

其基本思想是,每一行都可以在窗口中参与自己的日期、后一天或后一天。因此,首先将行扩展为三行,并附加不同的日期,然后它可以在计算的日期上使用常规的
COUNT(DISTINCT)
聚合。
HAVING
子句只是为了避免返回单独计算且不在基础数据中的日期的结果

with cte(Date, Item) as (
    select cast(a as datetime), b 
    from (values 
        ('01/01/2018','A')
        ,('01/01/2018','B')
        ,('02/01/2018','C')
        ,('03/01/2018','C')
        ,('04/01/2018','C')) t(a,b)
)

select 
    [Date] = dateadd(dd, n, Date), [Count] = count(distinct Item)
from 
    cte
    cross join (values (0),(1),(2)) t(n)
group by dateadd(dd, n, Date)
having max(iif(n = 0, 1, 0)) = 1

option (force order)
输出:

| DateCol                 | C |
|-------------------------|---|
| 2018-01-01 00:00:00.000 | 2 |
| 2018-01-02 00:00:00.000 | 3 |
| 2018-01-03 00:00:00.000 | 3 |
| 2018-01-04 00:00:00.000 | 1 |
|        Date             | Count |
|-------------------------|-------|
| 2018-01-01 00:00:00.000 |   2   |
| 2018-01-02 00:00:00.000 |   3   |
| 2018-01-03 00:00:00.000 |   3   |
| 2018-01-04 00:00:00.000 |   1   |
如果有许多重复行,则速度可能会更快:

select 
    [Date] = dateadd(dd, n, Date), [Count] = count(distinct Item)
from 
    (select distinct Date, Item from cte) c
    cross join (values (0),(1),(2)) t(n)
group by dateadd(dd, n, Date)
having max(iif(n = 0, 1, 0)) = 1

option (force order)
由于不支持(按[日期]划分的分区)上的
COUNT(DISTINCT item)
,您可以使用
densite\u rank
来模拟:

SELECT Date, dense_rank() over (partition by [Date] order by [item]) 
+ dense_rank() over (partition by [Date] order by [item] desc) 
- 1 as count_distinct_item
FROM myTable 
需要注意的一点是,
densite\u-rank
将按null计数,而
count
将不按null计数


有关更多详细信息,请参阅本文。

这里有一个简单的解决方案,它使用myTable本身作为分组日期的来源(为SQLServer dateadd编辑)。注意,此查询假设myTable中每个日期至少有一条记录;如果没有任何日期,则即使有前2天的记录,也不会出现在查询结果中:

select
    date,
    (select
        count(distinct item)
        from (select distinct date, item from myTable) as d2
     where
        d2.date between dateadd(day,-2,d.date) and d.date
    ) as count
from (select distinct date from myTable) as d

我用数学解决这个问题

z(任意一天)=3x+y(y为模式3值) 我需要从3*(x-1)+y+1到3*(x-1)+y+3

3*(x-1)+y+1=3*(z/3-1)+z%3+1

在这种情况下;我可以使用分组方式(介于3*(z/3-1)+z%3+1和z之间)

如果您需要其他日组,您可以使用

declare @n int = 4 (another day count)

SELECT  iif(OrderDate between  @n * (cast(OrderDate as int) / @n - 1) + (cast(OrderDate as int) % @n) + 1 
and orderdate, Orderdate, 0)
, count(sh.SalesOrderID) FROM Sales.SalesOrderDetail shd
JOIN Sales.SalesOrderHeader sh on sh.SalesOrderID = shd.SalesOrderID
group by iif(OrderDate between  @n * (cast(OrderDate as int) / @n - 1) + (cast(OrderDate as int) % @n) + 1 
and orderdate, Orderdate, 0)
order by iif(OrderDate between  @n * (cast(OrderDate as int) / @n - 1) + (cast(OrderDate as int) % @n) + 1 
and orderdate, Orderdate, 0)

谢谢,这给了我一点。我希望每次约会都能收到这个。对不起,我的答案怎么了?你能发布一些样本数据和你需要的结果吗?1)你的查询中的“天”是什么?2) 添加了问题中的示例。我不想要最后3天。我想要滚动3天内的每个日期的唯一项目。每一天是你想要增加或减少的单位,可以是月,也可以是年。但看起来在添加了样本数据和奖金后,您现在得到了更好的答案。谢谢。然而,它似乎很慢。我们是否可以想象加入3个表,每个表都有不同的延迟,并在加入的表上运行通常的计数?在
DateCol
上是否有索引?你看过执行计划了吗?交叉应用会更快。在任何情况下@RockScience,apply的工作速度都比使用LAG快得多。你可以在这方面做实验,并阅读大量相关文章。举一个例子,在您的一个类似案例中,在我的生产数据库中,约15行,使用apply运行5分钟,使用LAG需要3小时。谢谢。假设我的表名为myTable,您能澄清我应该运行的命令吗?现在我得到了错误`SQLServer数据库错误:“a”不是可识别的表提示选项。如果要将其作为表值函数或CHANGETABLE函数的参数,请确保将数据库兼容模式设置为90。`在上面的查询中,我使用了common table expression作为表,并填充了示例数据。这对你来说不是必要的。因此,您必须运行以
SELECT
语句开头的部分,并使用
myTable
更改
cte
。您的SQL Server版本是什么?非常感谢@Martin Smith为我的查询添加了说明交叉应用比使用交叉连接更快,因此在这样的情况下,如果您真的不想连接来自不同表的数据,请使用交叉应用更改交叉连接