【问题标题】:How to get missing dates with 0 value in SQL Server?如何在 SQL Server 中获取 0 值的缺失日期?
【发布时间】:2019-07-16 11:17:56
【问题描述】:

我在 SQL Server 中使用以下查询来查找 distinct 过去 7 天(不包括今天的日期)的登录次数:

SELECT TOP (7) CONVERT(date, LoginTime) AS ActivityDate, COUNT(DISTINCT LoginID) AS UserCount
FROM Login
WHERE CONVERT(date, LoginTime) < CONVERT(date, GETDATE())
GROUP BY CONVERT(date, LoginTime)  
ORDER BY ActivityDate DESC; 

它生成以下输出:

ActivityDate | UserCount
----------------------
2019-02-21   | 2
2019-02-20   | 3
2019-02-19   | 2
2019-02-15   | 2
2019-02-14   | 1
2019-02-13   | 2
2019-02-12   | 3

我的期望是所有最后 7 天都按顺序排列(不像当前输出,在 2019-02-19 之后缺少日期 2019-02-162019-02-172019-02-18)。我需要它,如果缺少日期,它必须以 0 计数显示。

我的预期输出如下:

ActivityDate | UserCount
----------------------
2019-02-21   | 2
2019-02-20   | 3
2019-02-19   | 2
2019-02-18   | 0
2019-02-17   | 0
2019-02-16   | 0
2019-02-15   | 2

【问题讨论】:

  • Do Login 表在LoginTime 列中有那些无登录日期;或以某种方式;你想要一个相当 smart 的查询将这些日期添加到结果中吗?
  • 使用Calendar Table
  • @vahdet LoginTime 具有 datetime 数据类型。

标签: sql sql-server tsql outer-join gaps-and-islands


【解决方案1】:

要查看特定值,该值必须来自一行。因此,要查看登录表中不存在的日期,您必须将它们生成为行somewhere

您可以使用简单的递归 CTE 在特定间隔之间每天生成 1 行,然后使用 LEFT JOIN 加入在该特定日期匹配的登录。不匹配的仍然会显示,因为我们使用的是LEFT JOIN

DECLARE @GeneratingDateFrom DATE = DATEADD(DAY, -7, GETDATE())
DECLARE @GeneratingDateTo DATE = GETDATE()

;WITH GeneratedDates AS
(
    SELECT
        GeneratedDate = @GeneratingDateFrom

    UNION ALL

    SELECT
        GeneratedDate = DATEADD(DAY, 1, G.GeneratedDate)
    FROM
        GeneratedDates AS G
    WHERE
        DATEADD(DAY, 1, G.GeneratedDate) < @GeneratingDateTo
)
SELECT
    G.GeneratedDate,
    count(distinct L.LoginID) as UserCount
FROM 
    GeneratedDates AS G
    LEFT JOIN [Login] AS L ON G.GeneratedDate = CONVERT(date, L.LoginTime)
GROUP BY
    G.GeneratedDate
ORDER BY 
    G.GeneratedDate desc

【讨论】:

  • 递归 CTE 是 worst ways to generate a sequential list 之一。只有 7 天,这不太可能成为问题,但对于希望将此解决方案扩展到数年/数十年的任何未来读者来说,这根本无法很好地扩展。
  • @GarethD true,在这种情况下,日历表或带有DATEADD 的数字会更好。
  • 取决于您所说的“最差”是什么意思。 CTE 方法通过完全独立和免维护而得到缓解,它不依赖其他表或视图来提供日期。您需要考虑整体性能,而不仅仅是生成日期的速度
  • @Cato 我发布链接的文章中的大多数解决方案也是完全独立的,并且不依赖于其他表。最快的方法是改编 Itzik Ben-Gan 的“Stacked CTE”,它提供了您所说的所有优点,而没有 RBAR 循环的开销。递归 CTE 只不过是 while 循环的语法糖,如果您不使用 WHILE 循环来执行任务,则不应使用递归 CTE。
  • @GarethD - 使用系统表,这些系统表将来可能会减少行数,这对于关键工作来说是有问题的,我将它们归类为不是真正独立的。堆叠的 CTE 对于生成大量行是有意义的,我可能想要平衡其更大的代码大小与我想要生成的行数。如果我查询一年,我应该可以让计算机数到 365,我不太可能需要 50000,即约 140 年
【解决方案2】:

你可以试试这个。在这里,您需要获取第一个最小日期和最大日期。之后,您需要生成这两天之间的所有日期。最后你需要加入两个表。

declare @MinDate date
declare @MaxDate date

select * into #temp from(
select top (7) CONVERT(date,LoginTime) as ActivityDate,count(distinct LoginID) as UserCount
        from Login
        where CONVERT(date,LoginTime )< convert(date,getdate())
        group by CONVERT(date,LoginTime )  
        order by ActivityDate desc; 
)a        

Set @MinDate = (select min (ActivityDate) from #temp)
Set @MaxDate = (select max (ActivityDate) from #temp)  

Select a.Date, isnull(b.UserCount,0) as UserCount from(
SELECT  TOP (DATEDIFF(DAY, @MinDate, @MaxDate) + 1)
        Date = DATEADD(DAY, ROW_NUMBER() OVER(ORDER BY a.object_id) - 1, @MinDate)
FROM    sys.all_objects a
        CROSS JOIN sys.all_objects b;
)a left join #temp b on a.Date = b.ActivityDate

您可以找到现场演示Here。我已将您的查询输出插入到临时表中,但逻辑相同。

【讨论】:

    【解决方案3】:

    只有 7 天,所以只需输入这些日期:

    SELECT ActivityDate, COUNT(DISTINCT LoginID) AS UserCount
    FROM (VALUES
        (CAST(CURRENT_TIMESTAMP - 1 AS DATE)), -- build the list of dates
        (CAST(CURRENT_TIMESTAMP - 2 AS DATE)),
        (CAST(CURRENT_TIMESTAMP - 3 AS DATE)),
        (CAST(CURRENT_TIMESTAMP - 4 AS DATE)),
        (CAST(CURRENT_TIMESTAMP - 5 AS DATE)),
        (CAST(CURRENT_TIMESTAMP - 6 AS DATE)),
        (CAST(CURRENT_TIMESTAMP - 7 AS DATE))
    ) datelist(ActivityDate)
    LEFT JOIN Login ON CAST(LoginTime AS DATE) = ActivityDate
    GROUP BY ActivityDate
    ORDER BY ActivityDate DESC
    

    【讨论】:

    • where 子句不是必需的,实际上使查询无法按预期工作,因为它将左连接变为内连接。
    • 哦,是的。已移除。谢谢。
    • 假设稍后,我被要求做一个月。那么我应该改变我的解决方案还是开始写 30 条语句?解决方案应该是动态的
    • 在这种情况下,只需使用数字表/数字生成器。
    • sqlperformance.com/2013/01/t-sql-queries/generate-a-set-1 解释了各种技术。只需将FROM (VALUES ...) datelist 替换为此处列出的解决方案之一。对于小于 1024 的数字,我通常更喜欢 spt_values。
    【解决方案4】:

    生成表中没有行的日期以加入calendar table 的最佳方法。

    这是一个非常简单的一年日历表,基于this answer:

    CREATE TABLE [Calendar]
    (
        [CalendarDate] DATETIME
    )
    
    DECLARE @StartDate DATETIME
    DECLARE @EndDate DATETIME
    SET @StartDate = GETDATE()
    SET @EndDate = DATEADD(d, 365, @StartDate)
    
    WHILE @StartDate <= @EndDate
          BEGIN
                 INSERT INTO [Calendar]
                 (
                       CalendarDate
                 )
                 SELECT
                       @StartDate
    
                 SET @StartDate = DATEADD(dd, 1, @StartDate)
          END
    

    (您可以修改此查询以在将来添加更多日期,以便暂时不需要维护。)

    现在您可以像这样在查询中加入日历表:

    select top (7) c.CalendarDate as ActivityDate,count(distinct LoginID) as UserCount
    from Calendar c
    left join Login l
        ON c.CalendarDate = CONVERT(date, l.LoginTime)
        and CONVERT(date,LoginTime )< convert(date,getdate())
    group by c.CalendarDate 
    order by c.CalendarDate desc; 
    

    它占用的空间是值得的,它在许多其他情况下也会派上用场。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-06-07
      • 1970-01-01
      • 1970-01-01
      • 2019-07-20
      • 1970-01-01
      相关资源
      最近更新 更多