在TSQL中生成递增date的结果集
考虑需要创builddate的结果集。 我们有开始date和结束date,我们希望在中间生成一个date列表。
DECLARE @Start datetime ,@End datetime DECLARE @AllDates table (@Date datetime) SELECT @Start = 'Mar 1 2009', @End = 'Aug 1 2009' --need to fill @AllDates. Trying to avoid looping. -- Surely if a better solution exists.
考虑使用WHILE
循环的当前实现:
DECLARE @dCounter datetime SELECT @dCounter = @Start WHILE @dCounter <= @End BEGIN INSERT INTO @AllDates VALUES (@dCounter) SELECT @dCounter=@dCounter+1 END
问题:如何使用T-SQL创build一组在用户定义的范围内的date? 假设SQL 2005+。 如果您的答案是使用SQL 2008function,请标记为这样。
如果你的date不超过2047天:
declare @dt datetime, @dtEnd datetime set @dt = getdate() set @dtEnd = dateadd(day, 100, @dt) select dateadd(day, number, @dt) from (select distinct number from master.dbo.spt_values where name is null ) n where dateadd(day, number, @dt) < @dtEnd
以下使用recursionCTE(SQL Server 2005+):
WITH dates AS ( SELECT CAST('2009-01-01' AS DATETIME) 'date' UNION ALL SELECT DATEADD(dd, 1, t.date) FROM dates t WHERE DATEADD(dd, 1, t.date) <= '2009-02-01') SELECT ... FROM TABLE t JOIN dates d ON d.date = t.date --etc.
为了使这个方法起作用,你需要做一次表设置:
SELECT TOP 10000 IDENTITY(int,1,1) AS Number INTO Numbers FROM sys.objects s1 CROSS JOIN sys.objects s2 ALTER TABLE Numbers ADD CONSTRAINT PK_Numbers PRIMARY KEY CLUSTERED (Number)
Numbers表设置完成后,使用以下查询:
SELECT @Start+Number-1 FROM Numbers WHERE Number<=DATEDIFF(day,@Start,@End)+1
捕捉他们呢:
DECLARE @Start datetime ,@End datetime DECLARE @AllDates table (Date datetime) SELECT @Start = 'Mar 1 2009', @End = 'Aug 1 2009' INSERT INTO @AllDates (Date) SELECT @Start+Number-1 FROM Numbers WHERE Number<=DATEDIFF(day,@Start,@End)+1 SELECT * FROM @AllDates
输出:
Date ----------------------- 2009-03-01 00:00:00.000 2009-03-02 00:00:00.000 2009-03-03 00:00:00.000 2009-03-04 00:00:00.000 2009-03-05 00:00:00.000 2009-03-06 00:00:00.000 2009-03-07 00:00:00.000 2009-03-08 00:00:00.000 2009-03-09 00:00:00.000 2009-03-10 00:00:00.000 .... 2009-07-25 00:00:00.000 2009-07-26 00:00:00.000 2009-07-27 00:00:00.000 2009-07-28 00:00:00.000 2009-07-29 00:00:00.000 2009-07-30 00:00:00.000 2009-07-31 00:00:00.000 2009-08-01 00:00:00.000 (154 row(s) affected)
@ KM的答案首先创build一个数字表,并用它来select一个date范围。 如果没有临时号码表,请执行以下操作:
DECLARE @Start datetime ,@End datetime DECLARE @AllDates table (Date datetime) SELECT @Start = 'Mar 1 2009', @End = 'Aug 1 2009'; WITH Nbrs_3( n ) AS ( SELECT 1 UNION SELECT 0 ), Nbrs_2( n ) AS ( SELECT 1 FROM Nbrs_3 n1 CROSS JOIN Nbrs_3 n2 ), Nbrs_1( n ) AS ( SELECT 1 FROM Nbrs_2 n1 CROSS JOIN Nbrs_2 n2 ), Nbrs_0( n ) AS ( SELECT 1 FROM Nbrs_1 n1 CROSS JOIN Nbrs_1 n2 ), Nbrs ( n ) AS ( SELECT 1 FROM Nbrs_0 n1 CROSS JOIN Nbrs_0 n2 ) SELECT @Start+n-1 as Date FROM ( SELECT ROW_NUMBER() OVER (ORDER BY n) FROM Nbrs ) D ( n ) WHERE n <= DATEDIFF(day,@Start,@End)+1 ;
testing当然,如果你经常这样做,永久性表格可能会更好的performance。
上面的查询是本文的一个修改版本,它讨论了生成序列并给出了许多可能的方法。 我喜欢这个,因为它不会创build临时表,并且不限于sys.objects
表中元素的sys.objects
。
尝试这个。 没有循环,CTE的限制等,你可以有几乎没有。 生成的logging。 根据需要pipe理交叉连接和顶层。
select top 100000 dateadd(d,incr,'2010-04-01') as dt from (select incr = row_number() over (order by object_id, column_id), * from ( select a.object_id, a.column_id from sys.all_columns a cross join sys.all_columns b ) as a ) as b
请注意嵌套是为了更容易控制和转换为视图等。
另一种select是在.NET中创build相应的函数。 以下是它的样子:
[Microsoft.SqlServer.Server.SqlFunction( DataAccess = DataAccessKind.None, FillRowMethodName = "fnUtlGetDateRangeInTable_FillRow", IsDeterministic = true, IsPrecise = true, SystemDataAccess = SystemDataAccessKind.None, TableDefinition = "d datetime")] public static IEnumerable fnUtlGetDateRangeInTable(SqlDateTime startDate, SqlDateTime endDate) { // Check if arguments are valid int numdays = Math.Min(endDate.Value.Subtract(startDate.Value).Days,366); List<DateTime> res = new List<DateTime>(); for (int i = 0; i <= numdays; i++) res.Add(dtStart.Value.AddDays(i)); return res; } public static void fnUtlGetDateRangeInTable_FillRow(Object row, out SqlDateTime d) { d = (DateTime)row; }
这基本上是一个原型,它可以做得更聪明,但说明了这个想法。 从我的经验来看,对于一个小到中等的时间跨度来说(比如几年),这个函数比在T-SQL中实现的更好。 CLR版本的另一个不错的function是它不会创build临时表。
概观
这是我的版本(2005兼容)。 这种方法的优点是:
- 你会得到一个通用的function,你可以使用一些类似的场景; 不限于date
- 范围不受现有表格的内容的限制
- 您可以轻松更改增量(例如,每7天获取date而不是每天)
- 你不需要访问其他目录(即主)
- SQL引擎能够做一些优化的TVF,它不能与一个while语句
- generate_series被用在其他一些dbs中,所以这可能有助于让你的代码本能地为更多的用户所熟悉
SQL小提琴: http ://sqlfiddle.com/#!6/c3896/1
码
基于给定参数生成一系列数字的可重用函数:
create function dbo.generate_series ( @start bigint , @stop bigint , @step bigint = 1 , @maxResults bigint = 0 --0=unlimitted ) returns @results table(n bigint) as begin --avoid infinite loop (ie where we're stepping away from stop instead of towards it) if @step = 0 return if @start > @stop and @step > 0 return if @start < @stop and @step < 0 return --ensure we don't overshoot set @stop = @stop - @step --treat negatives as unlimited set @maxResults = case when @maxResults < 0 then 0 else @maxResults end --generate output ;with myCTE (n,i) as ( --start at the beginning select @start , 1 union all --increment in steps select n + @step , i + 1 from myCTE --ensure we've not overshot (accounting for direction of step) where (@maxResults=0 or i<@maxResults) and ( (@step > 0 and n <= @stop) or (@step < 0 and n >= @stop) ) ) insert @results select n from myCTE option (maxrecursion 0) --sadly we can't use a variable for this; however checks above should mean that we have a finite number of recursions / @maxResults gives users the ability to manually limit this --all good return end
把它用于你的场景:
declare @start datetime = '2013-12-05 09:00' ,@end datetime = '2014-03-02 13:00' --get dates (midnight) --, rounding <12:00 down to 00:00 same day, >=12:00 to 00:00 next day --, incrementing by 1 day select CAST(n as datetime) from dbo.generate_series(cast(@start as bigint), cast(@end as bigint), default, default) --get dates (start time) --, incrementing by 1 day select CAST(n/24.0 as datetime) from dbo.generate_series(cast(@start as float)*24, cast(@end as float)*24, 24, default) --get dates (start time) --, incrementing by 1 hour select CAST(n/24.0 as datetime) from dbo.generate_series(cast(@start as float)*24, cast(@end as float)*24, default, default)
2005兼容
- 公用表expression式: http : //technet.microsoft.com/en-us/library/ms190766(v=sql.90).aspx
- 选项MaxRecursion提示: http ://technet.microsoft.com/en-us/library/ms181714(v=sql.90) .aspx
- 表值函数: http : //technet.microsoft.com/en-us/library/ms191165(v=sql.90).aspx
- 默认参数: http : //technet.microsoft.com/en-us/library/ms186755(v=sql.90).aspx
- date时间: http : //technet.microsoft.com/en-us/library/ms187819( v= sql.90).aspx
- 投射: http : //technet.microsoft.com/en-us/library/aa226054(v=sql.90).aspx
使用从0到两个date之间的差异的整数创build一个临时表。
SELECT DATE_ADD(@Start, INTERVAL tmp_int DAY) AS the_date FROM int_table;
我使用以下内容:
SELECT * FROM dbo.RangeDate(GETDATE(), DATEADD(d, 365, GETDATE())); -- Generate a range of up to 65,536 contiguous DATES CREATE FUNCTION dbo.RangeDate ( @date1 DATE = NULL , @date2 DATE = NULL ) RETURNS TABLE AS RETURN ( SELECT D = DATEADD(d, AN, CASE WHEN @date1 <= @date2 THEN @date1 ELSE @date2 END) FROM dbo.RangeSmallInt(0, ABS(DATEDIFF(d, @date1, @date2))) A ); -- Generate a range of up to 65,536 contiguous BIGINTS CREATE FUNCTION dbo.RangeSmallInt ( @num1 BIGINT = NULL , @num2 BIGINT = NULL ) RETURNS TABLE AS RETURN ( WITH Numbers(N) AS ( SELECT N FROM(VALUES (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1) -- 16 , (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1) -- 32 , (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1) -- 48 , (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1) -- 64 , (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1) -- 80 , (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1) -- 96 , (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1) -- 112 , (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1) -- 128 , (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1) -- 144 , (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1) -- 160 , (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1) -- 176 , (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1) -- 192 , (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1) -- 208 , (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1) -- 224 , (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1) -- 240 , (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1) -- 256 ) V (N) ) SELECT TOP ( CASE WHEN @num1 IS NOT NULL AND @num2 IS NOT NULL THEN ABS(@num1 - @num2) + 1 ELSE 0 END ) ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) + CASE WHEN @num1 <= @num2 THEN @num1 ELSE @num2 END - 1 FROM Numbers A , Numbers B WHERE ABS(@num1 - @num2) + 1 < 65537 );
这与已经提出的许多解决scheme并不完全相同,但是我喜欢它的几个方面:
- 没有要求的表格
- 参数可以以任何顺序传递
- 限制65,536个date是任意的,并且可以通过交换到一个函数(如RangeInt)来轻松扩展
这个解决scheme基于对MySQL相同问题的奇妙回答。 在MSSQL上也是非常高效的。 https://stackoverflow.com/a/2157776/466677
select DateGenerator.DateValue from ( select DATEADD(day, - (aa + (10 * ba) + (100 * ca) + (1000 * da)), CONVERT(DATE, GETDATE()) ) as DateValue from (select aa from (values (0),(1),(2),(3),(4),(5),(6),(7),(8),(9)) as a(a)) as a cross join (select ba from (values (0),(1),(2),(3),(4),(5),(6),(7),(8),(9)) as b(a)) as b cross join (select ca from (values (0),(1),(2),(3),(4),(5),(6),(7),(8),(9)) as c(a)) as c cross join (select da from (values (0),(1),(2),(3),(4),(5),(6),(7),(8),(9)) as d(a)) as d ) DateGenerator WHERE DateGenerator.DateValue BETWEEN 'Mar 1 2009' AND 'Aug 1 2009' ORDER BY DateGenerator.DateValue ASC
仅适用于过去的date,未来更改中的date减去DATEADD函数中的date。 查询只适用于SQL Server 2008+,但也可以通过将“从值select”构造replace为工会来重写。
我build议:创build一个数字辅助表,并使用它来生成date列表。 你也可以使用一个recursion的CTE,但是这可能不如join数字的辅助表格那样好。 有关这两个选项的信息,请参阅SQL,数字辅助表 。
虽然我真的很喜欢KM的解决scheme(+1),但是我必须质疑你的“无循环”的假设 – 考虑到你的应用程序可能的date范围,有一个循环不应该是很昂贵的。 主要的技巧是在staging / cache表中对循环的结果进行strore处理,这样,通过重新计算相同的确切date,极大的查询集合不会使系统变慢。 例如,每个查询只计算/caching尚不在caching中的date范围,并且需要(并预先填充一些实际的date范围,如约2年前的范围,范围由您的应用程序业务需求决定)。
最好的答案可能是使用CTE,但不能保证你能够使用它。 在我的情况下,我不得不将这个列表插入由查询生成器dinamically创build的现有查询…不能使用CTE或存储过程。
所以,Devio的回答是非常有用的,但我必须修改它才能在我的环境中工作。
如果您无法访问主数据库,则可以在数据库中使用另一个表。 就以前的例子而言,最大date范围由表格中select的行数给出。
在我的例子中,使用row_number,可以使用没有实际int列的表。
declare @bd datetime --begin date declare @ed datetime --end date set @bd = GETDATE()-50 set @ed = GETDATE()+5 select DATEADD(dd, 0, DATEDIFF(dd, 0, Data)) --date format without time from ( select (GETDATE()- DATEDIFF(dd,@bd,GETDATE())) --Filter on the begin date -1 + ROW_NUMBER() over (ORDER BY [here_a_field]) AS Data from [Table_With_Lot_Of_Rows] ) a where Data < (@ed + 1) --filter on the end date
真的很喜欢Devio的解决scheme,因为我需要这样的东西,需要在SQL Server 2000上运行(所以不能使用CTE),但是如何修改它以仅生成与给定的一周中的datealignment的date。 例如,我只希望与周一,周三和周五一致的date或者我select的任何特定顺序基于以下数字Scheme:
Sunday = 1 Monday = 2 Tuesday = 3 Wednesday = 4 Thursday = 5 Friday = 6 Saturday = 7
例:
StartDate = '2015-04-22' EndDate = '2017-04-22' --2 years worth Filter on: 2,4,6 --Monday, Wednesday, Friday dates only
我想要编码是添加两个额外的字段:day,day_code然后筛选条件生成的列表…
我想出了以下几点:
declare @dt datetime, @dtEnd datetime set @dt = getdate() set @dtEnd = dateadd(day, 1095, @dt) select dateadd(day, number, @dt) as Date, DATENAME(DW, dateadd(day, number, @dt)) as Day_Name into #generated_dates from (select distinct number from master.dbo.spt_values where name is null ) n where dateadd(day, number, @dt) < @dtEnd select * from #generated_dates where Day_Name in ('Saturday', 'Friday') drop table #generated_dates
我喜欢CTE,因为它很容易阅读和维护
Declare @mod_date_from date =getdate(); Declare @mod_date_to date =dateadd(year,1,@mod_date_from); with cte_Dates as ( SELECT @mod_date_from as reqDate UNION ALL SELECT DATEADD(DAY,1,reqDate) FROM cte_Dates WHERE DATEADD(DAY,1,reqDate) < @mod_date_to ) SELECT * FROM cte_Dates OPTION(MAXRECURSION 0);
不要忘记设置MAXRECURSION
这一个应该工作。
从sysobjects中selectTop 1000 DATEADD(d,ROW_NUMBER()OVER(ORDER BY ID),getdate())