如何根据 T-SQL 中前几个月的数据确定缺失月份的值

How to Determine Values for Missing Months based on Data of Previous Months in T-SQL

提问人:John Saunders 提问时间:5/1/2009 最后编辑:pilcrowJohn Saunders 更新时间:4/20/2016 访问量:5301

问:

我有一组在特定时间点发生的事务:

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

请注意,对于每个月,我们要么获取事务中指定的值,要么获取最新的非 null 值。

我的问题是我不知道该怎么做!我只是一个“中级”级别的SQL开发人员,我不记得以前见过这样的事情。当然,我可以在程序中创建我想要的数据,或者使用游标,但我想知道是否有更好的、面向集合的方法来做到这一点。

我正在使用 SQL Server 2008,因此,如果任何新功能会有所帮助,我想听听。

P.S. 如果有人能想出更好的方法来陈述这个问题,甚至更好的主题行,我将不胜感激。我花了很长一段时间才决定,“传播”虽然很蹩脚,但却是我能想到的最好的。“抹黑”听起来更糟。

sql-server t-sql 间隙和孤岛

评论

0赞 DForck42 5/1/2009
我不太明白你想要求什么。您想将一个月的水平与另一个月的水平进行比较吗?或者某个日期跨度内的所有级别是什么?
0赞 John Saunders 5/1/2009
任何给定月份的有效水平是多少。例如,2/2009 值为 1,因为 1/2009 为 1,并且没有为 2 指定任何内容。2009 年 4 月的值为 2,因为没有 4 月的数据。
0赞 DForck42 5/1/2009
所以基本上,如果一个月没有级别,你想为该月插入一个等于上个月水平的值?
0赞 John Saunders 5/1/2009
是的。没有级别的上个月。我将用数据更新我的问题,使这一点更加清晰。
0赞 DForck42 5/1/2009
好吧,这更有意义

答:

1赞 Guy 5/1/2009 #1

我无法通过手机访问 BOL,所以这是一个粗略的指南......

首先,您需要为没有数据的月份生成缺失的行。您可以将 OUTER 联接到具有所需时间跨度的固定表或临时表,也可以从以编程方式创建的数据集(存储过程等)中使用 OUTER 联接

其次,您应该查看新的 SQL 2008 “分析”函数,例如 MAX(value) OVER ( partition clause ) 以获取以前的值。

(我知道 Oracle 可以做到这一点,因为我需要它来计算交易日期之间的复利计算 - 真的是同样的问题)

希望这为您指明了正确的方向......

(避免将其放入临时表中并在其上光标。太粗糙了!!)

评论

0赞 John Saunders 5/1/2009
我已经看过这个想法,但不知道要使用什么分区。
1赞 DForck42 5/1/2009 #2

这是我想出的

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

评论

0赞 John Saunders 5/1/2009
哪些空间?当我单击“编辑”时,它们似乎都存在。
0赞 DForck42 5/1/2009
奇怪。那么我的回答有帮助吗?
0赞 John Saunders 5/1/2009
我看起来它有效。我想更多地玩它,以及另一个答案。
1赞 Tom H 5/1/2009 #3

要以基于集合的方式执行此操作,您需要为所有数据或信息设置集合。在这种情况下,有一个被忽视的数据,即“有几月份?在数据库中将“日历”表和“数字”表作为实用程序表非常有用。

以下是使用这些方法之一的解决方案。第一部分代码设置日历表。您可以使用光标或手动或其他方式填充它,并且可以将其限制为您的业务所需的任何日期范围(回到 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

评论

0赞 John Saunders 5/1/2009
汤姆,这看起来很有趣,但是当我尝试时,它出现了重复的行 - 每个输入行两行。
0赞 Tom H 5/1/2009
不知道该说什么。我只是复制了你的代码并运行了它,然后复制了我的代码并运行它,我得到了 12 行,每个月一行。确保 Calendar 表格没有重复项。否则,它与约翰·吉布(John Gibb)给出的方法几乎相同。
0赞 John Saunders 5/1/2009
也许只是早上太早了,但我看不出你的两个左边的外连接和他的外连接之间的对应关系。你能告诉我它们是如何等价的,或者它们有什么不同吗?
0赞 Tom H 5/1/2009
对不起,我的意思是它们在使用实用程序表的方式上是相同的。他在第 1 个月到第 12 个月使用数字,而我在每个月的第一天使用日历。
4赞 John Gibb 5/1/2009 #4

我首先构建一个数字表,其中包含从 1 到 100 万左右的连续整数。一旦你掌握了窍门,它们就会派上用场。

例如,以下是获取 2008 年每个月的 1 号的方法:

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;

这应该能给你提供你要找的东西,但你可能不得不在谷歌上搜索一下,以找到创建数字表的最佳方法。

评论

0赞 John Saunders 5/1/2009
这看起来像是赢家。谢谢,约翰。
1赞 ewbi 5/19/2009 #5

约翰·吉布(John Gibb)发布了一个很好的答案,已经接受了,但我想稍微扩展一下:

  • 取消一年的限制,
  • 在更多 明确的方式,以及
  • 无需单独 数字表。

这种细微的变化使用递归公用表表达式来建立一组 Dates,该集表示 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);

评论

0赞 A-K 7/5/2009
在撰写本文时,递归 CTE 速度很慢。
1赞 Ron Savage 7/5/2009 #6

如果您经常进行这种类型的分析,您可能会对我为此目的而组合的 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

这是对这个问题的回答,其中也有一些示例输出。

评论

0赞 John Saunders 7/5/2009
罗恩,谢谢。事实证明,我不需要使用它 - 找到了一种不需要知道的方法 - 此外,我目前不在那里(或任何地方)工作。
0赞 Deepak Shaw 4/20/2016 #7

-----替代方式------

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