Comment déterminer les valeurs pour les mois manquants à partir des données des mois précédents dans T-SQL

StackOverflow https://stackoverflow.com/questions/808356

Question

J'ai un ensemble de transactions qui se produisent à des moments spécifiques:

CREATE TABLE Transactions (
    TransactionDate Date NOT NULL,
    TransactionValue Integer NOT NULL
)

Les données peuvent être:

INSERT INTO Transactions (TransactionDate, TransactionValue)
VALUES ('1/1/2009', 1)
INSERT INTO Transactions (TransactionDate, TransactionValue)
VALUES ('3/1/2009', 2)
INSERT INTO Transactions (TransactionDate, TransactionValue)
VALUES ('6/1/2009', 3)

En supposant que TransactionValue définisse un niveau, je dois savoir quel était le niveau entre les transactions. J'ai besoin de cela dans le contexte d'un ensemble de requêtes T-SQL, il serait donc préférable que je puisse obtenir un résultat comme celui-ci:

Month   Value
1/2009  1
2/2009  1
3/2009  2
4/2009  2
5/2009  2
6/2009  3

Notez comment, pour chaque mois, nous obtenons la valeur spécifiée dans la transaction ou la dernière valeur non nulle.

Mon problème est que je ne sais pas trop comment faire cela! Je ne suis qu'un "intermédiaire". développeur SQL de niveau, et je ne me souviens pas d’avoir jamais rien vu de tel. Naturellement, je pourrais créer les données que je veux dans un programme ou utiliser des curseurs, mais j'aimerais savoir s'il existe une meilleure façon de le faire, axée sur les ensembles.

J'utilise SQL Server 2008, donc si l'une des nouvelles fonctionnalités peut aider, j'aimerais en entendre parler.

P.S. Si quelqu'un pouvait penser à un meilleur moyen de poser cette question, voire à un meilleur sujet, je l'apprécierais beaucoup. Il m'a fallu un bon bout de temps pour décider que "répandre", bien que boiteux, était le meilleur que je puisse trouver. "frottis" semblait pire.

Était-ce utile?

La solution

Je commencerais par créer un tableau Numbers contenant des entiers séquentiels allant de 1 à un million environ. Ils sont vraiment utiles une fois que vous maîtrisez la situation.

Par exemple, voici comment obtenir le 1er de chaque mois en 2008:

select firstOfMonth = dateadd( month, n - 1, '1/1/2008')
from Numbers
where n <= 12;

Maintenant, vous pouvez combiner cela en utilisant OUTER APPLY pour trouver la transaction la plus récente pour chaque date comme suit:

with Dates as (
    select firstOfMonth = dateadd( month, n - 1, '1/1/2008')
    from Numbers
    where n <= 12
)
select d.firstOfMonth, t.TransactionValue
from Dates d
outer apply (
    select top 1 TransactionValue
    from Transactions
    where TransactionDate <= d.firstOfMonth
    order by TransactionDate desc
) t;

Cela devrait vous donner ce que vous cherchez, mais vous devrez peut-être un peu Google pour trouver le meilleur moyen de créer le tableau Numbers.

Autres conseils

voici ce que je suis venu avec

declare @Transactions table (TransactionDate datetime, TransactionValue int)

declare @MinDate datetime
declare @MaxDate datetime
declare @iDate datetime
declare @Month int
declare @count int
declare @i int
declare @PrevLvl int

insert into @Transactions (TransactionDate, TransactionValue)
select '1/1/09',1

insert into @Transactions (TransactionDate, TransactionValue)
select '3/1/09',2

insert into @Transactions (TransactionDate, TransactionValue)
select '5/1/09',3


select @MinDate = min(TransactionDate) from @Transactions
select @MaxDate = max(TransactionDate) from @Transactions

set @count=datediff(mm,@MinDate,@MaxDate)
set @i=1
set @iDate=@MinDate


while (@i<=@count)
begin

    set @iDate=dateadd(mm,1,@iDate)

    if (select count(*) from @Transactions where TransactionDate=@iDate) < 1
    begin

        select @PrevLvl = TransactionValue from @Transactions where TransactionDate=dateadd(mm,-1,@iDate)

        insert into @Transactions (TransactionDate, TransactionValue)
        select @iDate, @prevLvl

    end


    set @i=@i+1
end

select *
from @Transactions
order by TransactionDate

Pour ce faire, vous avez besoin d'ensembles pour toutes vos données ou informations. Dans ce cas, il existe les données négligées de "Quels mois y a-t-il?" Il est très utile de disposer d'un " Calendrier " table ainsi qu’un "Nombre" table dans les bases de données en tant que tables d’utilitaires.

Voici une solution utilisant l'une de ces méthodes. Le premier bit de code configure votre table de calendrier. Vous pouvez le remplir à l’aide d’un curseur, manuellement ou autrement, et vous pouvez le limiter à la plage de dates requise par votre entreprise (retour au 1900-01-01 ou tout juste au 1970-01-01 et aussi loin dans le futur). vouloir). Vous pouvez également ajouter d'autres colonnes utiles à votre entreprise.

CREATE TABLE dbo.Calendar
(
     date           DATETIME     NOT NULL,
     is_holiday     BIT          NOT NULL,
     CONSTRAINT PK_Calendar PRIMARY KEY CLUSTERED (date)
)

INSERT INTO dbo.Calendar (date, is_holiday) VALUES ('2009-01-01', 1)  -- New Year
INSERT INTO dbo.Calendar (date, is_holiday) VALUES ('2009-01-02', 1)
...

Maintenant, en utilisant ce tableau, votre question devient triviale:

SELECT
     CAST(MONTH(date) AS VARCHAR) + '/' + CAST(YEAR(date) AS VARCHAR) AS [Month],
     T1.TransactionValue AS [Value]
FROM
     dbo.Calendar C
LEFT OUTER JOIN dbo.Transactions T1 ON
     T1.TransactionDate <= C.date
LEFT OUTER JOIN dbo.Transactions T2 ON
     T2.TransactionDate > T1.TransactionDate AND
     T2.TransactionDate <= C.date
WHERE
     DAY(C.date) = 1 AND
     T2.TransactionDate IS NULL AND
     C.date BETWEEN '2009-01-01' AND '2009-12-31'  -- You can use whatever range you want

John Gibb a publié une bonne réponse, déjà acceptée, mais je voudrais en dire un peu plus à:

  • éliminez la limite d'un an,
  • exposer la plage de dates de manière plus détaillée de manière explicite, et
  • élimine le besoin de séparer tableau des nombres.

Cette légère variante utilise une expression de table commune récursive pour établir l'ensemble de dates représentant le premier de chaque mois le ou après les dates de et à définies dans DateRange. Notez l'utilisation de l'option MAXRECURSION pour empêcher un débordement de pile (!); ajustez au besoin pour tenir compte du nombre maximal de mois prévu. En outre, envisagez d’ajouter une autre logique d’assemblage de Dates pour prendre en charge les semaines, les trimestres et même au jour le jour.

with 
DateRange(FromDate, ToDate) as (
  select 
    Cast('11/1/2008' as DateTime), 
    Cast('2/15/2010' as DateTime)
),
Dates(Date) as (
  select 
    Case Day(FromDate) 
      When 1 Then FromDate
      Else DateAdd(month, 1, DateAdd(month, ((Year(FromDate)-1900)*12)+Month(FromDate)-1, 0))
    End
  from DateRange
  union all
  select DateAdd(month, 1, Date)
  from Dates
  where Date < (select ToDate from DateRange)
)
select 
  d.Date, t.TransactionValue
from Dates d
outer apply (
  select top 1 TransactionValue
  from Transactions
  where TransactionDate <= d.Date
  order by TransactionDate desc
) t
option (maxrecursion 120);

Si vous effectuez souvent ce type d'analyse, cette fonction SQL Server que je vous ai concue pourrait vous intéresser:

if exists (select * from dbo.sysobjects where name = 'fn_daterange') drop function fn_daterange;
go

create function fn_daterange
   (
   @MinDate as datetime,
   @MaxDate as datetime,
   @intval  as datetime
   )
returns table
--**************************************************************************
-- Procedure: fn_daterange()
--    Author: Ron Savage
--      Date: 12/16/2008
--
-- Description:
-- This function takes a starting and ending date and an interval, then
-- returns a table of all the dates in that range at the specified interval.
--
-- Change History:
-- Date        Init. Description
-- 12/16/2008  RS    Created.
-- **************************************************************************
as
return
   WITH times (startdate, enddate, intervl) AS
      (
      SELECT @MinDate as startdate, @MinDate + @intval - .0000001 as enddate, @intval as intervl
         UNION ALL
      SELECT startdate + intervl as startdate, enddate + intervl as enddate, intervl as intervl
      FROM times
      WHERE startdate + intervl <= @MaxDate
      )
   select startdate, enddate from times;

go

c’était une réponse à cette question question , qui contient également un exemple de sortie.

Je n'ai pas accès à BOL depuis mon téléphone, c'est donc un guide approximatif ...

Tout d'abord, vous devez générer les lignes manquantes pour les mois sans données. Vous pouvez utiliser une jointure OUTER dans une table fixe ou temporaire avec la période souhaitée ou dans un jeu de données créé par programme (proc stocké, etc.)

Deuxièmement, vous devriez examiner les nouvelles fonctions "analytiques" de SQL 2008, telles que MAX (valeur) OVER (clause de partition) pour obtenir la valeur précédente.

(Je sais qu'Oracle peut le faire parce que j'en avais besoin pour calculer les intérêts calculés entre les dates de transaction - le même problème, en réalité.)

J'espère que cela vous oriente dans la bonne direction ...

(Évitez de le jeter dans une table temporaire et de la faire glisser dessus. Trop grossier !!!)

----- Autre moyen ------

select 
    d.firstOfMonth,
    MONTH(d.firstOfMonth) as Mon,
    YEAR(d.firstOfMonth) as Yr, 
    t.TransactionValue
from (
    select 
        dateadd( month, inMonths - 1, '1/1/2009') as firstOfMonth 
        from (
            values (1), (2), (3), (4), (5), (7), (8), (9), (10), (11), (12)
        ) Dates(inMonths)
) d
outer apply (
    select top 1 TransactionValue
    from Transactions
    where TransactionDate <= d.firstOfMonth
    order by TransactionDate desc
) t
Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top