2

我有一组在特定时间点发生的交易:

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,所以如果有任何新功能有帮助,我很想听听。

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

4

7 回答 7

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;

这应该可以为您提供所需的内容,但您可能需要在 Google 上搜索一下才能找到创建 Numbers 表的最佳方法。

于 2009-04-30T22:47:10.350 回答
1

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

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

其次,您应该查看新的 SQL 2008 '分析'函数,如 MAX(value) OVER ( partition 子句 ) 以获得先前的值。

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

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

(避免将它扔到临时表中并在上面移动。太粗糙了!!!)

于 2009-04-30T19:07:23.827 回答
1

这就是我想出的

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
于 2009-04-30T19:10:17.440 回答
1

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

这是使用其中一种方法的解决方案。第一段代码设置您的日历表。您可以使用光标或手动或其他方式填充它,并且您可以将其限制在您的业务所需的任何日期范围内(回到 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
于 2009-04-30T20:30:34.610 回答
1

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

  • 取消一年的限制,
  • 以更明确的方式显示日期范围,以及
  • 无需单独的数字表。

这种细微的变化使用递归公用表表达式来建立代表每个月的第一天或之后的日期集,该日期是在 DateRange 中定义的开始日期和结束日期。请注意使用 MAXRECURSION 选项来防止堆栈溢出 (!);根据需要进行调整以适应预期的最大月数。此外,考虑添加替代 Dates 组装逻辑以支持周、季度甚至日常。

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);
于 2009-05-19T10:07:12.127 回答
1

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

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

于 2009-07-05T02:38:25.917 回答
0

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

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
于 2016-04-20T11:21:23.323 回答