web-dev-qa-db-fra.com

Générer un jeu de résultats de dates incrémentées en SQL

Considérez le besoin de créer un ensemble de résultats de dates. Nous avons les dates de début et de fin, et nous aimerions générer une liste de dates entre les deux.

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.

Considérez l'implémentation actuelle avec une boucle WHILE:

DECLARE @dCounter datetime
SELECT @dCounter = @Start
WHILE @dCounter <= @End
BEGIN
 INSERT INTO @AllDates VALUES (@dCounter)
 SELECT @dCounter=@dCounter+1 
END

Question: Comment créer un ensemble de dates dans une plage définie par l'utilisateur à l'aide de T-SQL? Supposons SQL 2005+. Si votre réponse utilise les fonctionnalités de SQL 2008, veuillez l'indiquer comme tel.

51
p.campbell

Si vos dates ne sont pas séparées de plus de 2047 jours:

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

J'ai mis à jour ma réponse après plusieurs demandes pour le faire. Pourquoi?

La réponse originale contenait la sous-requête

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

ce qui donne le même résultat, tel que je les ai testés sur SQL Server 2008, 2012 et 2016. 

Cependant, comme j'ai essayé d'analyser le code interne de MSSQL lors de l'interrogation à partir de spt_values, j'ai constaté que les instructions SELECT contiennent toujours la clause WHERE [type]='[magic code]'.

Par conséquent, j'ai décidé que, bien que la requête renvoie le résultat correct, elle fournit le résultat correct pour de mauvaises raisons:

Il est possible qu'une version future de SQL Server définisse une valeur [type] différente, qui a également NULL comme valeurs pour [name], en dehors de la plage 0-2047 ou même non contiguë, auquel cas le résultat serait tout simplement faux.

48
devio

Ce qui suit utilise un CTE récursif (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

Pour que cette méthode fonctionne, vous devez procéder de la manière suivante:

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)

Une fois la table des nombres configurée, utilisez cette requête:

SELECT
    @Start+Number-1
    FROM Numbers
    WHERE Number<=DATEDIFF(day,@Start,@End)+1

pour les capturer faire:

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

sortie:

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.

La réponse de @ KM crée d'abord un tableau de nombres et l'utilise pour sélectionner une plage de dates. Pour faire la même chose sans la table des numéros temporaires:

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 ;

Bien sûr, si vous faites cela souvent, une table permanente pourrait bien être plus performante.

La requête ci-dessus est une version modifiée de cet article , qui traite de la génération de séquences et donne de nombreuses méthodes possibles. J'ai aimé celui-ci car il ne crée pas de table temporaire et ne se limite pas au nombre d'éléments de la table sys.objects.

4
Chadwick

Essaye ça. Pas de bouclage, de limites CTE, etc. et vous pourriez avoir n'importe quel non. des enregistrements générés. Gérer les jointures croisées et haut en fonction de ce qui est requis.

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

Veuillez noter que l'imbrication est destinée à faciliter le contrôle et la conversion en vues, etc.

3
Kapil

Cette solution est basée sur une merveilleuse réponse à la même question pour MySQL. Il est également très performant sur 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

ne fonctionne que pour les dates antérieures, pour les dates de modifications futures moins la connexion dans la fonction DATEADD. Query ne fonctionne que pour SQL Server 2008+, mais peut également être réécrit pour 2005 en remplaçant la construction "select from values" par des unions.

2
Marek Gregor

Une autre option consiste à créer une fonction correspondante dans .NET. Voici à quoi ça ressemble:

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

Il s’agit d’un prototype et il peut être beaucoup plus intelligent, mais illustre bien l’idée. D'après mon expérience, cette fonction est plus performante que celle implémentée dans T-SQL, même si elle ne dure que quelques années. Une autre fonctionnalité intéressante de la version CLR est qu’elle ne crée pas de table temporaire.

2
AlexS

Vue d'ensemble

Voici ma version (compatible 2005). Les avantages de cette approche sont les suivants:

  • vous obtenez une fonction polyvalente que vous pouvez utiliser pour un certain nombre de scénarios similaires; non limité aux dates
  • la plage n'est pas limitée par le contenu d'une table existante
  • vous pouvez facilement modifier l'incrément (par exemple, obtenez la date tous les 7 jours au lieu de chaque jour)
  • vous n'avez pas besoin d'accéder à d'autres catalogues (c.-à-d. maître)
  • le moteur SQL est capable de faire une optimisation de la TVF qu'il ne pouvait pas avec une déclaration while
  • generate_series est utilisé dans d'autres dbs, cela peut donc aider à rendre votre code instinctivement familier à un public plus large

Fiddle SQL: http://sqlfiddle.com/#!6/c3896/1

Code

Une fonction réutilisable pour générer une plage de nombres en fonction de paramètres donnés:

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

Mettre cela à utiliser pour votre scénario:

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 Compatible

2
JohnLBevan

J'utilise les éléments suivants:

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

Ce n’est pas si différent de la plupart des solutions proposées à ce jour, mais j’aime bien plusieurs choses:

  • Aucune table requise
  • Les arguments peuvent être passés dans n'importe quel ordre
  • La limite de 65 536 dates est arbitraire et peut facilement être étendue en basculant sur une fonction telle que RangeInt.
1
Kittoes0124

J'aime le CTE car il est facile à lire et à entretenir 

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

N'oubliez pas de définir MAXRECURSION  

1
Shahab J

créer une table temporaire avec des entiers allant de 0 à la différence entre vos deux dates.

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

Ce que je recommanderais: créez une table auxiliaire de nombres et utilisez-la pour générer votre liste de dates. Vous pouvez également utiliser un CTE récursif, mais cela peut ne pas fonctionner aussi bien que rejoindre une table de nombres auxiliaire. Voir SQL, Tableau de nombres auxiliaire pour plus d'informations sur les deux options. 

0
Justin Grant

Bien que j'aime beaucoup la solution de KM ci-dessus (+1), je dois remettre en question votre hypothèse "pas de boucle" - étant donné les plages de dates plausibles avec lesquelles votre application fonctionnera, le fait d'avoir une boucle ne devrait pas coûter si cher. L'astuce principale consiste à explorer les résultats de la boucle dans la table staging/cache afin que des ensembles de requêtes extrêmement volumineux ne ralentissent pas le système en calculant à nouveau les mêmes dates exactes. Par exemple. chaque requête calcule/met en cache uniquement les plages de dates qui ne sont PAS déjà dans le cache et dont elle a besoin (et pré-remplissez le tableau avec une plage de dates réaliste, telle que ~ 2 ans à l'avance, avec une plage déterminée par les besoins de votre application).

0
DVK

Celui-ci devrait fonctionner.

sélectionnez Top 1000 DATEADD (d, ROW_NUMBER () OVER (ORDER BY Id), getdate ()) dans sysobjects

0
Otpidus

Vraiment, j'aime bien la solution de Devio, car j'avais besoin de quelque chose comme ceci, qui doit fonctionner sur SQL Server 2000 (donc, je ne peux pas utiliser CTE). Cependant, comment pourrait-elle être modifiée pour générer UNIQUEMENT des dates qui correspondent à un ensemble de jours de la semaine. Par exemple, je veux seulement les dates qui correspondent au lundi, mercredi et vendredi ou à une séquence particulière que je choisis en fonction du nombre suivant Scheme:

Sunday = 1
Monday = 2
Tuesday = 3
Wednesday = 4
Thursday = 5
Friday = 6
Saturday = 7

Exemple: 

StartDate = '2015-04-22' EndDate = '2017-04-22' --2 years worth
Filter on: 2,4,6 --Monday, Wednesday, Friday dates only

Ce que j'essaie de coder, c'est d'ajouter deux champs supplémentaires: day, day_code .__, puis filtrer la liste générée avec une condition ...

Je suis venu avec ce qui suit:

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

La meilleure solution est probablement d'utiliser le CTE, mais rien ne garantit que vous pourrez l'utiliser. Dans mon cas, j'ai dû insérer cette liste dans une requête existante créée de manière dynamique par un générateur de requête ... je ne pouvais utiliser ni CTE ni les procédures stockées.

Donc, la réponse de Devio était vraiment utile, mais je devais la modifier pour qu'elle fonctionne dans mon environnement.

Si vous n'avez pas accès à la base de données master, vous pouvez utiliser une autre table dans votre base de données. Comme dans l'exemple précédent, la plage de dates maximale est donnée par le nombre de lignes à l'intérieur du tableau choisi. 

Dans mon exemple difficile, en utilisant le row_number, vous pouvez utiliser des tables sans colonne int réelle. 

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