SQL:按年切片DateRange数据(Tricky)

时间:2011-10-14 00:18:27

标签: sql sql-server

我有这个非常棘手的问题,我现在试图解决它,

我有这个查询结果集

    SELECT * FROM Orders

    OrderID | OrderAmount | OrderDate | Expiry Date
    1        $100          2008-01-01    2009-12-31
    2        $200          2009-01-01    2010-12-31
    3        $300          2010-01-01    2011-12-31
    4        $3            2010-01-01    2010-06-31
    5        $400          2007-01-01    2009-05-31

现在,我如何通过OrderDate细分每个订单 - 每年的ExpiryDate日期范围

我想在RDLC报告中得到类似的结果

    ORDERS CONSUMED PER YEAR    

    OrderID  |  YEAR     |    Consumed Amount
     1         2008           $50
     1         2009           $50
     2         2009           $100
     2         2010           $100
     3         2010           $150
     3         2011           $150  
     4         2010           $3
     5         2007           $160
     5         2008           $160
     5         2009           $80   <---- another tricky part

计算基于术语,eq。 (2年期300美元意味着每年150美元)

如何在MS-SQL查询中执行此操作?

**我知道标题似乎不正确^^,我只是找不到合适的标题

编辑:添加更多样本和解释

4 个答案:

答案 0 :(得分:2)

如果这是SQL Server 2005或更高版本,则可以使用公用表表达式递归构建月份表,并使用它来计算每年消耗的订单量。

一些注意事项:

  1. 此解决方案假设时间段基于整月。方法是确定每月消费的订单金额,然后乘以该年度的月数。如果需要,您可以修改此值以使用每天消耗的金额。

  2. 在最后的select中,我从money投射到decimal,精度更高,以避免舍入问题。您可能需要根据自己的需要进行调整,但我不确定您是否能完全避免将问题四舍五入。

  3. OrderID 5的结果与您的样本结果不符。这是因为这个例子在2009年有5个月,而不是6个月。


  4. create table #Orders
    (
        OrderID int,
        OrderAmount money,
        OrderDate datetime,
        ExpiryDate datetime
    )
    
    insert into #Orders values(1, 100, '2008-01-01', '2009-12-31'),
                              (2, 200, '2009-01-01', '2010-12-31'),
                              (3, 300, '2010-01-01', '2011-12-31'),
                              (4, 3,   '2010-01-01', '2010-06-30'),
                              (5, 400, '2007-01-01', '2009-05-31')
    
    ;with cte_months
    as
    (
        select OrderID, OrderDate, year(OrderDate) OrderYear
        from #Orders
        union all
        select m.OrderID, dateadd(month, 1, m.OrderDate), year(dateadd(month, 1, m.OrderDate)) 
        from cte_months m
            inner join #Orders o on m.OrderID = o.OrderID
        where dateadd(month, 1, m.OrderDate) <= o.ExpiryDate
    )
    select m.OrderId, m.OrderYear, cast(sum(o.MonthlyAmount) as money) as ConsumedAmount
    from
    (
        select OrderID, cast(OrderAmount as decimal(12,6)) / (datediff(month, orderdate, ExpiryDate) + 1) as MonthlyAmount
        from #Orders
    ) o inner join cte_months m on o.OrderID = m.OrderID
    group by m.OrderID, m.OrderYear
    
    drop table #Orders
    

    结果:

    OrderId     OrderYear   ConsumedAmount
    ----------- ----------- ---------------------
    1           2008        50.00
    1           2009        50.00
    2           2009        100.00
    2           2010        100.00
    3           2010        150.00
    3           2011        150.00
    4           2010        3.00
    5           2007        165.5172
    5           2008        165.5172
    5           2009        68.9655
    

答案 1 :(得分:1)

从您的有限数据集中,每个订单产生两年的时间,您可以这样做:

SELECT orderId, Year(OrderDate) AS Year, orderamount/2 AS amount FROM orders
UNION
SELECT orderId, Year(Expiry Date) AS Year, orderamount/2 AS amount

假设您的产品最多两年(但可能只有一个)产生数据:

SELECT orderId, year, sum(amount)
FROM (
  SELECT orderId, Year(OrderDate) year, orderamount/2 amount FROM orders
  UNION
  SELECT orderId, Year(Expiry Date) year, orderamount/2 amount FROM orders
) GROUP By orderId, year

如果你想要产生任意数年,它确实变得棘手。以下是一些适用于DB2 / MSSQL的递归SQL:

WITH expanded_orders (orderid, year)
AS 
(
  SELECT orderid, year(OrderDate) FROM orders
  UNION ALL
  SELECT orderid, year + 1 FROM expanded_orders
  WHERE year + 1 <= (SELECT year(expiry date) FROM orders WHERE orders.orderid = exanded_orders.orderid)
)

SELECT exp.orderid, exp.year, 
  orders.amount / (SELECT count(*) FROM expanded_orders this WHERE this.orderid = exp.orderid)
FROM expanded_orders exp, orders
WHERE exp.orderid = orders.orderid

我没有测试查询,所以它可能有语法错误,但一般方法应该是合理的。

更新:我刚看到你添加的例子。这种方法在未完全使用的年份将不起作用,在这种情况下,第二选择必须适应于不均匀地划分条纹。但我认为这应该可以使用'传统'sql解决。

答案 2 :(得分:1)

正如inflagranti指出,如果Orders表所涵盖的年份中存在漏洞,我的第一个解决方案会遇到问题。我试图动态创建所需的年份。

更直接的解决方案是使用numbers or tally table来提供年份。我的计数表名为Number,并且有一个int类型的N列,其数字为0到1,000,000。使用计数表提供连接年份以获取跨越多年的订单的多行。对于ConsumedAmount,我正在计算OrderAmount *(订单的订单/天的年份中的天数。)如果您想要不同的东西,例如几个月或几周,或财政上的任何东西,您将需要修改。

注意我也使用SQL Server 2008功能,特别是2008年新增的date数据类型。从数字计数表中获取YearNumber,YearStartDate和YearEndDate的方法在2008之前会有所不同

请注意,由于闰年的额外日期,这些数字与您的示例略有不同。

; With Y as (select N as YearNumber
        , dateadd(year, N-1, cast('0001-01-01' as date)) as YearStartDate
        , dateadd(day, -1, dateadd(year, N, cast('0001-01-01' as date))) 
             as YearEndDate
    from Number)
select O.OrderId
    , Y.YearNumber
    -- Next column is the OrderAmount * the ratio of 
    -- number of days in this year for this order
    -- divided by the number of days of the order.
    , OrderAmount * -- multiply the Order amount by the ratio of
        ((case when year(OrderDate) = year(Expirydate)
                then datediff(day, OrderDate, ExpiryDate) 
            when YearNumber = year(OrderDate)
                then datediff(day, OrderDate, YearEndDate)
            when YearNumber = year(ExpiryDate)
                then datediff(day, YearStartDate, ExpiryDate)
            else datediff(day, YearStartDate, YearEndDate) end + 1)
        / cast(datediff(day, OrderDate, ExpiryDate) + 1 as float))
from Orders O
inner join Y on Y.YearNumber between year(OrderDate) and year(ExpiryDate)
order by O.OrderId, YearNumber

测试数据:

create table Orders (OrderId int not null constraint Orders_PK primary key
    , OrderAmount money
    , OrderDate date
    , ExpiryDate date)
go

insert into Orders
values (1, 100, '2008-01-01', '2009-12-31')
, (2, 200, '2009-01-01', '2010-12-31')
, (3, 300, '2010-01-01', '2011-12-31')
, (4, 3, '2010-01-01', '2010-06-30')
, (5, 400, '2007-01-01', '2009-05-31')
, (100, 1000, '2010-01-01', '2019-12-31')

查询结果:

OrderId     YearNumber  
----------- ----------- ----------------------
1           2008        50.0683994528044
1           2009        49.9316005471956
2           2009        100
2           2010        100
3           2010        150
3           2011        150
4           2010        3
5           2007        165.532879818594
5           2008        165.986394557823
5           2009        68.4807256235828
100         2010        99.9452354874042
100         2011        99.9452354874042
100         2012        100.219058050383
100         2013        99.9452354874042
100         2014        99.9452354874042
100         2015        99.9452354874042
100         2016        100.219058050383
100         2017        99.9452354874042
100         2018        99.9452354874042
100         2019        99.9452354874042

答案 3 :(得分:1)

另一种方法是使用它 - 这将在未来长达2047年使用,如果需要更多,某些数字表将是有用的,虽然我不认为这是有效的adrift的解决方案!

create table #Orders      (OrderID int,     OrderAmount money,     OrderDate datetime,     ExpiryDate datetime   )
insert into #Orders 
SELECT 1 as orderid, 100 as orderamount, '2008-01-01' as orderdate, '2009-12-31' as expirydate UNION ALL
SELECT 2, 200, '2009-01-01', '2010-12-31' UNION ALL
SELECT 3, 300, '2010-01-01', '2011-12-31' UNION ALL
SELECT 4, 3,   '2010-01-01', '2010-06-30' UNION ALL
SELECT 5, 400, '2007-01-01', '2009-05-31' 

SELECT 
orderid 
,orderamount as total_order_amount
,YEAR(orderdate) + number as orderyear
,CASE WHEN 
DATEDIFF(MM,orderdate,expirydate) - (12 * (YEAR(orderdate) - YEAR(orderdate) + number)) 
> 12 THEN 12 * CAST(orderamount / DATEDIFF(MM,orderdate,expirydate)  AS DECIMAL(18,5)) 
ELSE 
(DATEDIFF(MM,orderdate,expirydate) % 12 * 
CAST(orderamount / DATEDIFF(MM,orderdate,expirydate)  AS DECIMAL(18,5))) END as amount
,CAST(orderamount / DATEDIFF(MM,orderdate,expirydate)  AS DECIMAL(18,5)) as monthly_value
from #orders
LEFT OUTER JOIN
(
select number from spt_values
where type = 'p'
) numbers on numbers.number <= DATEDIFF(YY,orderdate,expirydate)