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

Posted

技术标签:

【中文标题】如何在 SQL Server 中的滑动窗口上聚合(计算不同的项目)?【英文标题】:How to aggregate (counting distinct items) over a sliding window in SQL Server? 【发布时间】:2018-06-23 16:34:36 【问题描述】:

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

SELECT Date, COUNT(DISTINCT item) 
FROM myTable 
GROUP BY Date 
ORDER BY Date

我如何转换它以获取每个日期过去 3 天中唯一项目的数量(包括当天)?

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

例如,如果原始表是:

Date        Item  
01/01/2018  A  
01/01/2018  B  
02/01/2018  C  
03/01/2018  C    
04/01/2018  C

通过上面的查询,我目前得到了每天的唯一计数:

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)    

【问题讨论】:

【参考方案1】:

使用GETDATE() 函数获取当前日期,使用DATEADD() 获取最近3 天

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

【讨论】:

谢谢,这给了我一分。我希望在每个日期都得到这个。 抱歉我的回答有什么问题?您可以发布一些示例数据以及您需要什么结果? 1) 您的查询中的“天”是什么? 2)添加了有问题的示例。我不想要最后 3 天。我想要每个日期滚动 3 天的独特项目 天是你要加减的单位,可以是月、年。但看起来在添加示例数据和赏金之后,您现在得到了更好的答案。【参考方案2】:

使用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

Results

|              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
;

您可以在该子查询中使用group by,而不是使用select distinct,但执行计划将保持不变。

Demo at SQL Fiddle

【讨论】:

谢谢。然而,它似乎很慢。我们是否可以想象加入 3 个表,每个表都有不同的延迟,并在加入的表上运行通常的不同计数? 您在DateCol 上有索引吗?你看过执行计划吗? 交叉应用会更快。在任何情况下@RockScience,在任何情况下应用都比使用 LAG 快得多。您可以对此进行试验并阅读大量相关文章。举个例子,在你的类似情况下,在我大约 15m 行的生产数据库中,使用 apply 在 5 分钟内运行,使用 LAG 需要 3 小时。【参考方案3】:

GROUP BY 应该比 DISTINCT 更快(确保在您的 Date 列上有一个索引)

DECLARE @tbl TABLE([Date] DATE, [Item] VARCHAR(100))
;

INSERT INTO @tbl  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');

SELECT t.[Date]

      --Just for control. You can take this part away
      ,(SELECT DISTINCT t2.[Item] AS [*]
        FROM @tbl AS t2
        WHERE t2.[Date]<=t.[Date] 
          AND t2.[Date]>=DATEADD(DAY,-2,t.[Date]) FOR XML PATH('')) AS CountedItems

      --This sub-select comes back with your counts 
      ,(SELECT COUNT(DISTINCT t2.[Item])
        FROM @tbl AS t2
        WHERE t2.[Date]<=t.[Date] 
          AND t2.[Date]>=DATEADD(DAY,-2,t.[Date])) AS ItemCount
FROM @tbl AS t
GROUP BY t.[Date];

结果

Date        CountedItems    ItemCount
2018-01-01  AB              2
2018-01-02  ABC             3
2018-01-03  ABC             3
2018-01-04  C               1

【讨论】:

【参考方案4】:

SQL

SELECT DISTINCT Date,
       (SELECT COUNT(DISTINCT item)
        FROM myTable t2
        WHERE t2.Date BETWEEN DATEADD(day, -2, t1.Date) AND t1.Date) AS count
FROM myTable t1
ORDER BY Date;

演示

Rextester 演示:http://rextester.com/ZRDQ22190

【讨论】:

【参考方案5】:

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

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 |

【讨论】:

【参考方案6】:

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

基本思想是每一行都可以在自己的日期、后天或后天参与窗口。因此,这首先将该行扩展为三行,并附加了这些不同的日期,然后它可以只使用常规的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)

输出:

|        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)

【讨论】:

谢谢。假设我的表名为 myTable ,您能否澄清我应该运行的命令?现在我收到错误`SQL Server 数据库错误:“a”不是可识别的表提示选项。如果它打算作为表值函数或 CHANGETABLE 函数的参数,请确保您的数据库兼容模式设置为 90。` 在上面的查询中,我使用公用表表达式作为您的表并填充了示例数据。这对你来说不是必需的。因此,您必须运行以SELECT 语句开头的部分并将cte 更改为myTable。您的 SQL Server 版本是多少? 非常感谢@Martin Smith 为我的查询添加描述 使用交叉应用比使用交叉连接更快,所以在你不想连接不同表的数据的情况下,用交叉应用更改交叉连接【参考方案7】:

由于不支持COUNT(DISTINCT item) OVER (PARTITION BY [Date]),您可以使用dense_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 

需要注意的一点是,dense_rank 将被视为 null,而 COUNT 则不会。

请参阅this 帖子了解更多详情。

【讨论】:

【参考方案8】:

这是一个简单的解决方案,它使用 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

【讨论】:

【参考方案9】:

我用数学解决了这个问题。

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

在这种情况下;我可以使用 group by (在 3* (z / 3 - 1) + z % 3 + 1 and z)

    SELECT  iif(OrderDate between  3 * (cast(OrderDate as int) / 3 - 1) + (cast(OrderDate as int) % 3) + 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  3 * (cast(OrderDate as int) / 3 - 1) + (cast(OrderDate as int) % 3) + 1 
and orderdate, Orderdate, 0)
order by iif(OrderDate between  3 * (cast(OrderDate as int) / 3 - 1) + (cast(OrderDate as int) % 3) + 1 
and orderdate, Orderdate, 0)

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

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)

【讨论】:

以上是关于如何在 SQL Server 中的滑动窗口上聚合(计算不同的项目)?的主要内容,如果未能解决你的问题,请参考以下文章

如何在NorthWind SQL Server上执行聚合?

SQL Server中的开窗函数是啥?

SQL Server 中的窗口函数(2012 新函数)

如何在 NorthWind SQL Server 上执行聚合?

sql server 2012 自定义聚合函数(MAX_O3_8HOUR_ND) 计算最大的臭氧8小时滑动平均值

在 KDB/Q 中按时滑动窗口