web-dev-qa-db-ja.com

SQLで日付の増分の結果セットを生成します

日付の結果セットを作成する必要があることを考慮してください。開始日と終了日があり、その間の日付のリストを生成したいと思います。

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を使用して、ユーザー定義の範囲内にある一連の日付をどのように作成しますか? SQL 2005+を想定しています。回答がSQL 2008の機能を使用している場合は、そのようにマークしてください。

52
p.campbell

日付の間隔が2047日以内の場合:

declare @dt datetime, @dtEnd datetime
set @dt = getdate()
set @dtEnd = dateadd(day, 100, @dt)

select dateadd(day, number, @dt)
from 
    (select number from master.dbo.spt_values
     where [type] = 'P'
    ) n
where dateadd(day, number, @dt) < @dtEnd

そうするようにいくつかの要求の後、答えを更新しました。なぜですか?

元の回答にはサブクエリが含まれていました

 select distinct number from master.dbo.spt_values
     where name is null

sQL Server 2008、2012、および2016でテストした結果、同じ結果が得られます。

ただし、spt_valuesからクエリを実行するときにMSSQLが内部的にコードを分析しようとすると、SELECTステートメントには常にWHERE [type]='[magic code]'句が含まれることがわかりました。

したがって、クエリは正しい結果を返しますが、間違った理由で正しい結果を提供することにしました。

SQL Serverの将来のバージョンでは、[type]の値としてNULLを含む異なる[name]値を定義する可能性があり、0-2047の範囲外、または非連続ですらあります、その場合、結果は単に間違っています。

50
devio

以下では、再帰CTE(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.
41
OMG Ponies

この方法が機能するには、このワンタイムテーブルのセットアップを行う必要があります。

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)
5
KM.

@KMの答えは、最初に数値テーブルを作成し、それを使用して日付の範囲を選択します。一時番号テーブルなしで同じことを行うには:

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 ;

もちろん、これを頻繁に行う場合は、永続テーブルの方がパフォーマンスが向上する可能性があります。

上記のクエリは、 この記事 からの修正版です。シーケンスの生成について説明し、多くの可能な方法を提供します。これは、一時テーブルを作成せず、sys.objectsテーブル内の要素の数に制限されないため、これが気に入りました。

4
Chadwick

これを試して。ルーピング、CTE制限などはありません。生成されたレコードの。必要に応じて、相互結合と上部を管理します。

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

ネストは、制御やビューへの変換などを簡単にするためのものです。

3
Kapil

読みやすく、メンテナンスが簡単なので、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の設定を忘れないでください

2
Shahab J

このソリューションは、MySQLに対する同じ質問に対するすばらしい回答に基づいています。また、MSSQLでも非常に優れたパフォーマンスを発揮します。 https://stackoverflow.com/a/2157776/466677

select DateGenerator.DateValue from (
  select DATEADD(day, - (a.a + (10 * b.a) + (100 * c.a) + (1000 * d.a)), CONVERT(DATE, GETDATE()) ) as DateValue
  from (select a.a from (values (0),(1),(2),(3),(4),(5),(6),(7),(8),(9)) as a(a)) as a
  cross join (select b.a from (values (0),(1),(2),(3),(4),(5),(6),(7),(8),(9)) as b(a)) as b
  cross join (select c.a from (values (0),(1),(2),(3),(4),(5),(6),(7),(8),(9)) as c(a)) as c
  cross join (select d.a 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

過去の日付、将来の日付、DATEADD関数のマイナス記号でのみ機能します。クエリはSQL Server 2008+でのみ機能しますが、「値から選択」構文をユニオンに置き換えることにより、2005年でも書き換えることができます。

2
Marek Gregor

別のオプションは、対応する関数を.NETで作成することです。これは次のようになります。

[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バージョンのもう1つの素晴らしい機能は、一時テーブルを作成しないことです。

2
AlexS

概要

これが私のバージョンです(2005互換)。このアプローチの利点は次のとおりです。

  • 多くの同様のシナリオで使用できる汎用機能を取得します。日付だけに限らない
  • 範囲は既存のテーブルの内容によって制限されません
  • 増分を簡単に変更できます(たとえば、毎日ではなく7日ごとに日付を取得します)
  • 他のカタログ(マスター)へのアクセスは必要ありません
  • sQLエンジンは、whileステートメントではできなかったTVFの最適化を行うことができます
  • generate_seriesは他の一部のデータベースで使用されているため、コードをより多くのユーザーに本能的に馴染ませることができます。

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 (i.e. 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互換

2
JohnLBevan

私は次を使用します:

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, A.N, 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
);

すでに提案されている多くのソリューションとそれほど違いはありませんが、私が気に入っている点がいくつかあります。

  • テーブルは必要ありません
  • 引数は任意の順序で渡すことができます
  • 65,536日付の制限は任意であり、RangeIntなどの関数に交換することで簡単に拡張できます
1
Kittoes0124

0から2つの日付の差までの整数で一時テーブルを作成します。

SELECT DATE_ADD(@Start, INTERVAL tmp_int DAY) AS the_date FROM int_table;
1
dnagirl

これは動作するはずです。

sysobjectsから上位1000個のDATEADD(d、ROW_NUMBER()OVER(ORDER BY Id)、getdate())を選択します

1
Otpidus

推奨事項:数値の補助テーブルを作成し、それを使用して日付のリストを生成します。再帰的なCTEを使用することもできますが、補助的な数値テーブルへの結合と同様に機能しない場合があります。両方のオプションの詳細については、 SQL、数値の補助表 を参照してください。

0
Justin Grant

上記のKMのソリューションが本当に気に入っています(+1)が、「ループなし」という前提に疑問を呈する必要があります。アプリが動作するもっともらしい日付範囲を考えると、ループはそれほど高価ではないはずです。主なトリックは、ステージング/キャッシュテーブルでループの結果を取得することです。これにより、非常に大きなクエリセットが同じ正確な日付を再計算してシステムの速度を低下させることはありません。例えば。各クエリは、まだキャッシュに存在しない必要な日付範囲のみを計算/キャッシュします(アプリケーションのビジネスニーズによって決定される範囲で、2年前などの現実的な日付範囲をテーブルに事前設定します)。

0
DVK

SQL Server 2000で実行する必要がある(つまりCTEを使用できない)まさにこのようなものが必要だったので、Devioのソリューションが本当に好きですが、特定の曜日に合わせた日付のみを生成するように修正する方法はありますか?たとえば、月曜日、水曜日、金曜日、または次の番号スキームに基づいて選択した特定のシーケンスに一致する日付のみが必要です。

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

私がコーディングしようとしているのは、2つの追加フィールドを追加することです: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
0
Leo

最善の答えはおそらくCTEを使用することですが、それを使用できるという保証はありません。私の場合、クエリジェネレーターによって動的に作成された既存のクエリ内にこのリストを挿入する必要がありました... CTEもストアドプロシージャも使用できませんでした。

そのため、Devioからの回答は本当に役に立ちましたが、自分の環境で動作するように修正する必要がありました。

マスターデータベースにアクセスできない場合は、データベース内の別のテーブルを使用できます。前の例と同様に、最大日付範囲は、選択されたテーブル内の行数によって指定されます。

難しい例では、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
0
fgpx78