Cómo determinar los valores de los meses faltantes según los datos de los meses anteriores en T-SQL
-
03-07-2019 - |
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.
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