Cómo determinar los valores de los meses faltantes según los datos de los meses anteriores en T-SQL

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

Pregunta

Tengo un conjunto de transacciones que ocurren en puntos específicos en el tiempo:

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

Los datos pueden ser:

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)

Suponiendo que TransactionValue establece algún tipo de nivel, necesito saber cuál fue el nivel entre las transacciones. Necesito esto en el contexto de un conjunto de consultas T-SQL, por lo que sería mejor si pudiera obtener un conjunto de resultados como este:

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

Observe cómo, para cada mes, obtenemos el valor especificado en la transacción o obtenemos el valor no nulo más reciente.

¡Mi problema es que tengo poca idea de cómo hacer esto! Solo soy un " intermedio " Desarrollador SQL de nivel, y no recuerdo haber visto nada como esto antes. Naturalmente, podría crear los datos que quiero en un programa, o usar cursores, pero me gustaría saber si hay una forma mejor y más orientada a hacer esto.

Estoy usando SQL Server 2008, por lo que si alguna de las nuevas funciones me ayudará, me gustaría saberlo.

P.S. Si alguien puede pensar en una mejor manera de plantear esta pregunta, o incluso en una línea de asunto mejor, lo apreciaría enormemente. Me tomó bastante tiempo decidir que " propagación " ;, mientras que cojo, fue lo mejor que pude encontrar. " Frotis " sonaba peor.

¿Fue útil?

Solución

Comenzaría por construir una tabla de números que contenga enteros secuenciales de 1 a un millón o menos. Son muy útiles una vez que aprendes el tema.

Por ejemplo, aquí se explica cómo obtener el primer día de cada mes en 2008:

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

Ahora, puede juntar eso utilizando APLICACIÓN EXTERNA para encontrar la transacción más reciente para cada fecha, de este 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;

Esto debería proporcionarte lo que estás buscando, pero es posible que tengas que buscar un poco en Google para encontrar la mejor manera de crear la tabla de números.

Otros consejos

Esto es lo que se me ocurrió

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

Para hacerlo de forma basada en conjuntos, necesita conjuntos para todos sus datos o información. En este caso, los datos pasados ??por alto de " ¿Qué meses hay? & Quot; Es muy útil tener un " Calendario " tabla y un " Número " Tabla en bases de datos como tablas de utilidad.

Aquí hay una solución usando uno de estos métodos. El primer bit de código configura tu tabla de calendario. Puede llenarlo usando un cursor o manualmente o lo que sea, y puede limitarlo a cualquier intervalo de fechas que sea necesario para su negocio (de 1900-01-01 o simplemente de 1970-01-01 y hasta el futuro, tanto como lo necesite). desear). También puede agregar cualquier otra columna que sea útil para su negocio.

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

Ahora, al usar esta tabla, tu pregunta se vuelve trivial:

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 publicó una buena respuesta, ya aceptada, pero quería ampliarla un poco para:

  • eliminar la limitación de un año,
  • exponer el rango de fechas en una más forma explícita, y
  • eliminar la necesidad de un separado tabla de números.

Esta ligera variación utiliza una expresión de tabla común recursiva para establecer el conjunto de Fechas que representan el primero de cada mes en o después de y hasta las fechas definidas en DateRange. Tenga en cuenta el uso de la opción MAXRECURSION para evitar un desbordamiento de pila (!); ajuste según sea necesario para adaptarse al número máximo de meses esperado. Además, considere la posibilidad de agregar una lógica de ensamblaje de fechas alternativa para admitir semanas, trimestres, incluso el día a día.

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 realiza este tipo de análisis a menudo, podría interesarle esta función de SQL Server que reuní exactamente para este propósito:

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

fue una respuesta a esto pregunta , que también tiene algunos resultados de muestra.

No tengo acceso a BOL desde mi teléfono, por lo que esta es una guía aproximada ...

Primero, necesita generar las filas faltantes para los meses en que no tiene datos. Puede utilizar una combinación EXTERNA a una tabla fija o tabla temporal con el intervalo de tiempo que desee o desde un conjunto de datos creado mediante programación (proc almacenado o similar)

Segundo, debe ver las nuevas funciones 'analíticas' de SQL 2008, como MAX (value) OVER (cláusula de partición) para obtener el valor anterior.

(SÉ que Oracle puede hacer esto porque lo necesitaba para calcular los cálculos de intereses compuestos entre las fechas de las transacciones; el mismo problema realmente)

Espero que esto te apunte en la dirección correcta ...

(Evita arrojarlo a una tabla temporal y poner un cursor sobre él. ¡Demasiado burdo!)

----- Forma alternativa ------

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
Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top