Come determinare i valori per i mesi mancanti in base ai dati dei mesi precedenti in T-SQL
-
03-07-2019 - |
Domanda
Ho una serie di transazioni che si verificano in momenti specifici nel tempo:
CREATE TABLE Transactions (
TransactionDate Date NOT NULL,
TransactionValue Integer NOT NULL
)
I dati potrebbero essere:
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)
Supponendo che TransactionValue imposti un tipo di livello, devo sapere quale era il livello tra le transazioni. Ne ho bisogno nel contesto di una serie di query T-SQL, quindi sarebbe meglio se potessi ottenere un set di risultati come questo:
Month Value
1/2009 1
2/2009 1
3/2009 2
4/2009 2
5/2009 2
6/2009 3
Nota come, per ogni mese, otteniamo il valore specificato nella transazione o otteniamo il valore non null più recente.
Il mio problema è che non ho idea di come farlo! Sono solo un "intermedio" sviluppatore SQL di livello e non ricordo di aver mai visto nulla di simile prima d'ora. Ovviamente, potrei creare i dati desiderati in un programma o utilizzare i cursori, ma vorrei sapere se esiste un modo migliore e orientato al set per farlo.
Sto usando SQL Server 2008, quindi se una qualsiasi delle nuove funzionalità sarà di aiuto, mi piacerebbe saperlo.
P.S. Se qualcuno può pensare a un modo migliore per formulare questa domanda, o anche a una linea tematica migliore, lo apprezzerei molto. Mi ci è voluto un po 'di tempo per decidere che "spread", mentre zoppo, era il migliore che potessi inventare. & Quot; smear " suonava peggio.
Soluzione
Comincerei costruendo una tabella Numbers che contenesse numeri interi sequenziali da 1 a un milione circa. Sono molto utili una volta capito.
Ad esempio, ecco come ottenere il 1 ° di ogni mese nel 2008:
select firstOfMonth = dateadd( month, n - 1, '1/1/2008')
from Numbers
where n <= 12;
Ora puoi metterlo insieme usando OUTER APPLY per trovare la transazione più recente per ogni data in questo modo:
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;
Questo dovrebbe darti quello che stai cercando, ma potresti dover cercare un po 'con Google per trovare il modo migliore per creare la tabella Numbers.
Altri suggerimenti
ecco cosa mi è venuto in mente
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
Per farlo in un modo basato su set, sono necessari set per tutti i tuoi dati o informazioni. In questo caso ci sono i dati trascurati di " Quali mesi ci sono? & Quot; È molto utile avere un " Calendario " tabella e un "Numero" tabella nei database come tabelle di utilità.
Ecco una soluzione che utilizza uno di questi metodi. Il primo bit di codice imposta la tabella del calendario. Puoi riempirlo usando un cursore o manualmente o qualsiasi altra cosa e puoi limitarlo a qualunque intervallo di date sia necessario per la tua attività (indietro al 1900-01-01 o appena al 1970-01-01 e fino al futuro come te volere). Puoi anche aggiungere altre colonne utili per la tua attività.
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)
...
Ora, usando questa tabella la tua domanda diventa banale:
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 ha pubblicato una risposta eccellente, già accettata, ma volevo ampliarla un po 'per:
- elimina la limitazione di un anno,
- espone l'intervallo di date in più modo esplicito e
- elimina la necessità di un separato tabella dei numeri.
Questa leggera variazione utilizza una espressione di tabella comune ricorsiva per stabilire l'insieme di date che rappresentano il primo di ogni mese a partire da e verso le date definite in DateRange. Notare l'uso dell'opzione MAXRECURSION per evitare un overflow dello stack (!); adeguarsi se necessario per adattarsi al numero massimo di mesi previsto. Inoltre, considera l'aggiunta di una logica di assemblaggio di date alternative per supportare settimane, trimestri, anche quotidianamente.
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);
Se esegui spesso questo tipo di analisi, potresti essere interessato a questa funzione di SQL Server che ho creato esattamente per questo scopo:
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
è stata una risposta a questa domanda , che ha anche alcuni esempi di output.
Non ho accesso a BOL dal mio telefono, quindi questa è una guida approssimativa ...
Innanzitutto, devi generare le righe mancanti per i mesi in cui non hai dati. Puoi utilizzare un join ESTERNO a una tabella fissa o temporanea con l'intervallo di tempo che desideri o da un set di dati creato a livello di codice (proc memorizzato o simile)
In secondo luogo, è necessario esaminare le nuove funzioni "analitiche" di SQL 2008, come MAX (valore) OVER (clausola di partizione) per ottenere il valore precedente.
(So che Oracle può farlo perché ne avevo bisogno per calcolare i calcoli degli interessi composti tra le date delle transazioni - lo stesso problema in realtà)
Spero che questo ti indichi nella giusta direzione ...
(Evita di gettarlo in una tabella temporanea e di imprecare su di esso. Troppo grezzo !!!)
----- Modo alternativo ------
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