Как определить значения для пропущенных месяцев на основе данных предыдущих месяцев в T-SQL - PullRequest
2 голосов
/ 30 апреля 2009

У меня есть набор транзакций, происходящих в определенные моменты времени:

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

Данные могут быть:

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)

Предполагая, что TransactionValue устанавливает какой-то уровень, мне нужно знать, какой уровень был между транзакциями. Мне это нужно в контексте набора запросов T-SQL, поэтому было бы лучше, если бы я мог получить такой набор результатов:

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

Обратите внимание, как для каждого месяца мы либо получаем значение, указанное в транзакции, либо получаем самое последнее ненулевое значение.

Моя проблема в том, что я мало представляю, как это сделать! Я всего лишь разработчик SQL "промежуточного" уровня, и я не помню, чтобы когда-либо видел что-то подобное раньше. Естественно, я мог бы создавать нужные мне данные в программе или с помощью курсоров, но я хотел бы знать, есть ли лучший, ориентированный на наборы способ сделать это.

Я использую SQL Server 2008, поэтому, если какие-то новые функции помогут, я бы хотел услышать об этом.

P.S. Если кто-то может придумать лучший способ сформулировать этот вопрос или даже лучшую тему, я был бы очень признателен. Мне потребовалось много времени, чтобы решить, что «спред», хотя и хромой, был лучшим, что я мог придумать. «Мазок» звучал хуже.

Ответы [ 7 ]

4 голосов
/ 01 мая 2009

Я бы начал с создания таблицы Numbers, содержащей последовательные целые числа от 1 до миллиона или около того. Они очень пригодятся, когда вы овладеете им.

Например, вот как получить 1-е число каждого месяца в 2008 году:

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

Теперь вы можете собрать это вместе, используя OUTER APPLY, чтобы найти самую последнюю транзакцию для каждой даты, например, так:

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;

Это должно дать вам то, что вы ищете, но вам, возможно, придется немного поискать в Google, чтобы найти лучший способ создать таблицу чисел.

1 голос
/ 05 июля 2009

Если вы выполняете этот тип анализа часто, вас может заинтересовать функция SQL Server, которую я собрал именно для этой цели:

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

это был ответ на этот вопрос , который также имеет некоторый пример выходных данных.

1 голос
/ 19 мая 2009

Джон Гибб опубликовал хороший ответ, уже принятый, но я хотел бы немного расширить его до:

  • устранить ограничение на один год,
  • выставить диапазон дат в более явным образом и
  • исключить необходимость отдельного таблица чисел.

Это небольшое изменение использует рекурсивное общее табличное выражение , чтобы установить набор дат, представляющих первое число каждого месяца, начиная с даты и до дат, определенных в DateRange. Обратите внимание на использование опции MAXRECURSION для предотвращения переполнения стека (!); отрегулируйте по мере необходимости, чтобы учесть максимальное количество ожидаемых месяцев. Кроме того, рассмотрите возможность добавления альтернативной логики сборки дат для поддержки недель, кварталов и даже повседневных задач.

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);
1 голос
/ 01 мая 2009

Чтобы сделать это на основе набора, вам нужны наборы для всех ваших данных или информации. В этом случае есть пропущенные данные "Какие месяцы там?" Очень полезно иметь таблицу «Календарь», а также таблицу «Число» в базах данных в качестве служебных таблиц.

Вот решение с использованием одного из этих методов. Первый бит кода настраивает вашу таблицу календаря. Вы можете заполнить его с помощью курсора или вручную или как угодно, и вы можете ограничить его тем диапазоном дат, который необходим для вашего бизнеса (вернуться к 1900-01-01 или просто назад к 1970-01-01 и так же далеко в будущем, как вы хочу). Вы также можете добавить любые другие столбцы, которые полезны для вашего бизнеса.

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

Теперь, используя эту таблицу, ваш вопрос становится тривиальным:

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
1 голос
/ 30 апреля 2009

вот что я придумал

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
1 голос
/ 30 апреля 2009

У меня нет доступа к BOL с моего телефона, так что это грубое руководство ...

Сначала вам нужно сгенерировать пропущенные строки за месяцы, в которых у вас нет данных. Вы можете использовать OUTER-соединение с фиксированной или временной таблицей с желаемым временным интервалом или из программно созданного набора данных (сохраненный процесс или тому подобное)

Во-вторых, вы должны взглянуть на новые аналитические функции SQL 2008, такие как MAX (значение) OVER (предложение раздела), чтобы получить предыдущее значение.

(Я ЗНАЮ, что Oracle может сделать это, потому что мне нужно было рассчитать сложный процентный расчет между датами транзакций - та же проблема на самом деле)

Надеюсь, это направит вас в правильном направлении ...

(Избегайте бросать его во временную таблицу и использовать курсоры над ним. Слишком грубо!)

0 голосов
/ 20 апреля 2016

----- Альтернативный способ ------

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