Comment déterminer les valeurs pour les mois manquants à partir des données des mois précédents dans T-SQL
-
03-07-2019 - |
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.
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