没有光标可能的每日收入总和?

时间:2010-11-16 14:41:56

标签: sql-server-2005 cursor

我有一张表格,用于存储一些资产的到期收入表 该表给出了新收入金额生效的日期以及每日收入金额。

我想计算2个日期之间的总收入。

这是表结构和示例数据:

DECLARE @incomeschedule  
TABLE (asset_no int, start_date datetime, amt decimal(14,2),
      PRIMARY KEY (asset_no, start_date))
/* 
-- amt is the amount of daily income
-- start_date is the effective date, from when that amt starts to be come in
*/

INSERT INTO @incomeschedule (asset_no, start_date, amt)
VALUES (1, '1 Jan 2010', 3)
INSERT INTO @incomeschedule (asset_no, start_date, amt)
VALUES (1, '1 Jul 2010', 4)
INSERT INTO @incomeschedule (asset_no, start_date, amt)
VALUES (1, '1 Oct 2010', 5)
INSERT INTO @incomeschedule (asset_no, start_date, amt)
VALUES (2, '1 Jan 2010', 1)
INSERT INTO @incomeschedule (asset_no, start_date, amt)
VALUES (2, '1 Jan 2012', 2)
INSERT INTO @incomeschedule (asset_no, start_date, amt)
VALUES (2, '1 Jan 2014', 4)
INSERT INTO @incomeschedule (asset_no, start_date, amt)
VALUES (2, '1 Jan 2016', 5)

因此,对于资产1,从1月1日起每天收入3美元,从7月1日起增至4美元,从10月1日起增至5美元。

对于2010年1月1日至2020年12月31日期间的总收入计算,以资产1为例,我们有 - 在3美元(2010年1月1日至2010年6月30日)的181天= 543美元 - 加上92天4美元(2010年7月1日至2010年9月30日)= $ 368
- 加上37天5美元(2010年10月1日至2020年12月31日)= 18720美元 - 总计19631美元 [同样,资产2的价格为14242美元]

因此,对于2010年1月1日至2020年12月31日的输入范围,我期望得到以下结果:

asset_no    total_amt
    1      19631.00
    2      14242.00

我使用游标编写了这个[因为我需要知道以前的行值来执行计算]但是我想知道是否可以使用基于集合的技术生成这些结果。

这是基于游标的代码,如果有帮助的话。

DECLARE @date_from datetime,
        @date_to datetime

SET @date_from = '1 Jan 2010'
SET @date_to = '31 Dec 2020'

/*-- output table to store results */
DECLARE @incomeoutput TABLE (asset_no int PRIMARY KEY, total_amt decimal(14,2))

/*-- cursor definition */
DECLARE c CURSOR FAST_FORWARD FOR 
SELECT asset_no, start_date, amt 
FROM @incomeschedule
UNION 
/* insert dummy records to zeroise from @date_from, 
   in case this is earlier than initial start_date per asset */
SELECT DISTINCT asset_no, @date_from, 0
FROM @incomeschedule
WHERE NOT EXISTS (SELECT asset_no, start_date FROM @incomeschedule WHERE start_date <= @date_from)
ORDER BY asset_no, start_date

/*-- initialise loop variables */
DECLARE @prev_asset_no int, @dummy_no int
SET @dummy_no = -999 /* arbitrary value, used to detect that we're in the first iteration */ 
SET @prev_asset_no = @dummy_no

DECLARE @prev_date datetime
SET @prev_date = @date_from

DECLARE @prev_amt decimal(14,2)
SET @prev_amt = 0

DECLARE @prev_total decimal(14,2)
SET @prev_total = 0

DECLARE @asset_no int, @start_date datetime, @amt decimal(14,2)

/*-- read values from cursor */
OPEN c
FETCH NEXT FROM c INTO @asset_no, @start_date, @amt
WHILE @@FETCH_STATUS = 0 
    BEGIN
        /*-- determine whether we're looking at a new asset or not */
        IF @prev_asset_no = @asset_no -- same asset: increment total and update loop variables
            BEGIN
                SET @prev_asset_no = @asset_no
                SET @prev_total = @prev_total + (@prev_amt * DATEDIFF(d, @prev_date, @start_date))
                SET @prev_date = @start_date
                SET @prev_amt = @amt
            END
        ELSE /*-- new asset: output record and reset loop variables */
            BEGIN
                IF @prev_asset_no <> @dummy_no /*-- first time round, we don't need to output */
                    BEGIN
                        SET @prev_total = @prev_total + (@prev_amt * DATEDIFF(d, @prev_date, @date_to))
                        INSERT INTO @incomeoutput (asset_no, total_amt) VALUES (@prev_asset_no, @prev_total)
                    END
                SET @prev_asset_no = @asset_no
                SET @prev_total = 0
                SET @prev_date = @start_date
                SET @prev_amt = @amt
            END

        FETCH NEXT FROM c INTO @asset_no, @start_date, @amt
    END

SET @prev_total = @prev_total + (@prev_amt * DATEDIFF(d, @prev_date, @date_to))
INSERT INTO @incomeoutput (asset_no, total_amt) VALUES (@prev_asset_no, @prev_total)

CLOSE c
DEALLOCATE c

SELECT asset_no, total_amt 
FROM @incomeoutput

n.b。我确实考虑过将基于游标的解决方案作为答案发布,以避免问题膨胀......但我说的问题的方式我需要一个基于非游标的答案,所以这感觉就像是更好的方法。如果这不是正确的礼节,请评论。

2 个答案:

答案 0 :(得分:2)

select i1.asset_no, 
    sum(i1.amt * cast(isnull(i2.start_date, '2020-12-31') - i1.start_date as int)) as total_amt
from @incomeschedule i1
left outer join @incomeschedule i2 on i1.asset_no = i2.asset_no 
    and i2.start_date = (
        select MIN(start_date) 
        from @incomeschedule 
        where start_date > i1.start_date 
            and asset_no = i1.asset_no
    )
group by i1.asset_no

答案 1 :(得分:1)

为什么要使用CTE?

declare @EndDate datetime
set @EndDate = '20201231'

select t1.asset_no,SUM(DATEDIFF(day,t1.start_date,COALESCE(t2.start_date,@EndDate))*t1.amt)
from
    @incomeschedule t1
        left join
    @incomeschedule t2
        on
            t1.asset_no = t2.asset_no and
            t1.start_date < t2.start_date
        left join
    @incomeschedule t3
        on
            t1.asset_no = t3.asset_no and
            t1.start_date < t3.start_date and
            t3.start_date < t2.start_date
where
    t3.asset_no is null
group by t1.asset_no

如果有些资产没有与您的范围的开始日期相同的初始条目,则查询稍微复杂一些(但也不会太糟糕)

(第3次连接到表(t3),空检查是为了确保t1和t2之间的匹配行是连续的)