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.

È stato utile?

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
Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top