如何在SQL Server中生成一系列日期

12
标题不能完全表达我的意思,并且可能是重复的。
这是长版本:给定客人的姓名,他们的注册日期和退房日期,如何为他们作为客人的每一天生成一行?
例如:Bob在7月14日入住并于7月17日离开。我想要...
('Bob', 7/14), ('Bob', 7/15), ('Bob', 7/16), ('Bob', 7/17) 
作为我的结果。
谢谢!

请看这里: https://dev59.com/EXM_5IYBdhLWcg3wNgOZ - StoicFnord
3
通常不这样做。您需要查找表并从中选择它们。WHERE calendar.date >= user.start_date AND calendar.date <= user.leave_date 您可以使用循环或递归查询生成集合,但它们永远不如使用查找表快速。 - MatBailie
请指定 SQL Server 的版本。我发布了一个依赖于 SQL Server 2008 的解决方案;如果您使用的是 SQL Server 2005,则可能会有所不同。 - Aaron Bertrand
我并无冒犯之意,真的。http://meta.stackexchange.com/q/122986/133242 - Matt Ball
可能是SQL Server 2008生成一系列日期时间的重复问题。 - 200_success
显示剩余3条评论
5个回答

34
我认为,对于这个特定的目的,下面的查询与使用专门的查找表相比几乎同样有效。
DECLARE @start DATE, @end DATE;
SELECT @start = '20110714', @end = '20110717';

;WITH n AS 
(
  SELECT TOP (DATEDIFF(DAY, @start, @end) + 1) 
    n = ROW_NUMBER() OVER (ORDER BY [object_id])
  FROM sys.all_objects
)
SELECT 'Bob', DATEADD(DAY, n-1, @start)
FROM n;

结果:

Bob     2011-07-14
Bob     2011-07-15
Bob     2011-07-16
Bob     2011-07-17

假设您需要将其作为一组使用,而不是单个成员,因此这里有一种适应此技术的方法:

DECLARE @t TABLE
(
    Member NVARCHAR(32), 
    RegistrationDate DATE, 
    CheckoutDate DATE
);

INSERT @t SELECT N'Bob', '20110714', '20110717'
UNION ALL SELECT N'Sam', '20110712', '20110715'
UNION ALL SELECT N'Jim', '20110716', '20110719';

;WITH [range](d,s) AS 
(
  SELECT DATEDIFF(DAY, MIN(RegistrationDate), MAX(CheckoutDate))+1,
    MIN(RegistrationDate)
    FROM @t -- WHERE ?
),
n(d) AS
(
  SELECT DATEADD(DAY, n-1, (SELECT MIN(s) FROM [range]))
  FROM (SELECT ROW_NUMBER() OVER (ORDER BY [object_id])
  FROM sys.all_objects) AS s(n)
  WHERE n <= (SELECT MAX(d) FROM [range])
)
SELECT t.Member, n.d
FROM n CROSS JOIN @t AS t
WHERE n.d BETWEEN t.RegistrationDate AND t.CheckoutDate;
----------^^^^^^^ not many cases where I'd advocate between!

结果:

Member    d
--------  ----------
Bob       2011-07-14
Bob       2011-07-15
Bob       2011-07-16
Bob       2011-07-17
Sam       2011-07-12
Sam       2011-07-13
Sam       2011-07-14
Sam       2011-07-15
Jim       2011-07-16
Jim       2011-07-17
Jim       2011-07-18
Jim       2011-07-19

正如 @Dems 指出的那样,这可以简化为:

;WITH natural AS 
(
  SELECT ROW_NUMBER() OVER (ORDER BY [object_id]) - 1 AS val 
  FROM sys.all_objects
) 
SELECT t.Member, d = DATEADD(DAY, natural.val, t.RegistrationDate) 
  FROM @t AS t INNER JOIN natural 
  ON natural.val <= DATEDIFF(DAY, t.RegistrationDate, t.CheckoutDate);

据我所知,SQL Server的优化器意味着您实际上不需要 WHERE n < = (SELECT MAX()),这意味着这可以进一步简化... WITH natural AS (SELECT ROW_NUMBER() OVER (ORDER BY id) - 1 AS val FROM sys.objects) SELECT t.Member, DATEADD(DAY, natural.val, t.start) FROM @t AS t INNER JOIN natural ON natural.val <= DATEDIFF(DAY, t.start, t.end) [但即便如此,一个直接查找表仍然至少会使用更少的CPU周期。] - MatBailie
谢谢,您的查询恰好做到了我想要的。有一个问题--在“range”表上使用MAX和MIN是必要的吗?在这个例子中,我只看到为“range”生成了一行,因此只有一个候选者可以成为最大值或最小值(在这种情况下,我会将范围和开始日期放在常规变量中)。我对您的SQL技巧印象深刻,并且很好奇是否有我错过的微妙之处。 - Daniel Cotter
这个适用于当你需要处理多个用户,且存在日期重叠的情况。如果你仅需要处理一个用户的单次访问,则完全不必使用该版本的查询。 - Aaron Bertrand
哦,我们的生产环境中有成千上万个客人和重叠日期,但对于 [range],仍然只返回一行数据。 - Daniel Cotter
好的,那个解释很有道理。起初我以为你是在说在某些情况下使用变量会得到不正确的结果,但现在看来更像是性能(或者风格)的问题。好的,非常感谢你的帮助。 - Daniel Cotter
显示剩余12条评论

8

我通常使用row_number()函数在某个表上实现此操作。因此:

select t.name, dateadd(d, seq.seqnum, t.start_date)
from t left outer join
     (select row_number() over (order by (select NULL)) as seqnum
      from t
     ) seq
     on seqnum <= datediff(d, t.start_date, t.end_date)

计算seq的速度很快,因为不需要计算或排序。然而,您需要确保表格足够大,以容纳所有时间跨度。

2
如果您有一个“Tally”或“Numbers”表格,那么像这样的事情就非常简单了。
 SELECT Member, DatePresent = DATEADD(dd,t.N,RegistrationDate)
   FROM @t 
  CROSS JOIN dbo.Tally t
  WHERE t.N BETWEEN 0 AND DATEDIFF(dd,RegistrationDate,CheckoutDate)
;

这里是如何构建“Tally”表格的方法。
--===================================================================
--      Create a Tally table from 0 to 11000
--===================================================================
--===== Create and populate the Tally table on the fly.
 SELECT TOP 11001
        IDENTITY(INT,0,1) AS N
   INTO dbo.Tally
   FROM Master.sys.ALL_Columns ac1
  CROSS JOIN Master.sys.ALL_Columns ac2
;
--===== Add a CLUSTERED Primary Key to maximize performance
  ALTER TABLE dbo.Tally
    ADD CONSTRAINT PK_Tally_N 
        PRIMARY KEY CLUSTERED (N) WITH FILLFACTOR = 100
;
--===== Allow the general public to use it
  GRANT SELECT ON dbo.Tally TO PUBLIC
;
GO

如果您想了解SQL中“Tally”表的含义以及如何使用它来替代While循环和递归CTE中的“Hidden RBAR”进行计数,请参阅以下文章。

http://www.sqlservercentral.com/articles/T-SQL/62867/


我真的很喜欢这个概念。如果有一个内置的虚拟表可以像这样连接(不会浪费任何内存或磁盘IO),那就太好了。如果您在SQL Server反馈表上建议添加虚拟Tally表功能,请将链接发送给我,我会为其投票! - Louis Somers
显然,有人已经这样做了:https://feedback.azure.com/forums/908035-sql-server/suggestions/32890519-add-a-built-in-table-of-numbers - Louis Somers

0

这可能适合你:

with mycte as
 (
     select cast('2000-01-01' as datetime) DateValue, 'Bob' as Name
     union all
     select DateValue + 1 ,'Bob' as Name
     from    mycte   
     where   DateValue + 1 < '2000-12-31'
 )
 select *
from    mycte
OPTION (MAXRECURSION 0)

2
其中包含了一个"计数递归CTE"。请参考以下文章,了解即使计算小数字时它们为什么如此糟糕。http://www.sqlservercentral.com/articles/T-SQL/74118/ - Jeff Moden

-6
我会创建一个触发器来创建额外的记录,并在结账时运行它。或者,您可以每天午夜运行相同的作业(如果您需要数据库中的最新信息)。

1
这并不是一个真正的答案 - 触发器如何“创建额外的记录”? - Aaron Bertrand
@AaronBertrand 这是任何编程语言中都很简单的编程任务。 - Andy
3
如果这很简单,那么提问者就不会提出问题了,对吧?而且用代码来支持你的回答也不应该很难吧,特指这种语言的代码。 - Aaron Bertrand
@AaronBertrand 我认为这不是关于实现,而是关于方法。 - Andy
1
我猜我们对“如何为每天生成一行客人记录”的解释不同。对我来说,这听起来像是一个关于具体语法的问题,而不是“去写一个查询”的问题。 <耸肩> - Aaron Bertrand
2
@Andy... 你写道:“@AaronBertrand 这是任何语言中都很简单的编程任务”。让我们看看你有什么。发布触发代码。 - Jeff Moden

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接