2010-11-16 8 views
1

J'ai un tableau qui stocke le calendrier des revenus dus, pour un certain nombre d'actifs.
Ce tableau indique la date d'entrée en vigueur d'un nouveau montant de revenu, ainsi que le montant de revenu quotidien.Somme du revenu journalier possible sans curseur?

Je veux calculer le revenu total dû entre 2 dates.

est ici la structure de la table et les échantillons:

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) 

Donc, pour l'actif 1, il est revenu $ 3 Tous les jours partir du 1er janvier, passant à 4 $ de 1 juil à 5 ​​$ du 1 octobre

Pour le calcul du revenu total entre le 1er janvier 2010 et le 31 décembre 2020, en utilisant l'actif 1 comme exemple, nous avons
- 181 jours à 3 $ (1er janvier 2010 au 30 juin 2010) = 543 $
- plus 92 jours à 4 $ (du 1er juillet 2010 au 30 sept. 2010) = 368 $
- en plus 3744 jours à 5 $ (1 octobre 2010 à 31 décembre 2020) = $ 18720
- au total 19631
$ [De même, l'actif 2 arrive à 14242 $]

Donc, pour une plage d'entrée Au 1 er janvier 2010 au 31 déc 2020, je me attends à la sortie suivante:

asset_no total_amt 
    1  19631.00 
    2  14242.00 

Je l'ai écrit en utilisant un curseur [comme je l'ai besoin de connaître les valeurs précédentes des lignes pour effectuer les CALC] mais je voudrais savoir s'il est possible de produire ces résultats en utilisant des techniques basées sur des ensembles.

Voici le code basé sur le curseur, au cas où cela serait utile.

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. J'ai envisagé de poster la solution basée sur le curseur comme une réponse, pour éviter de gonfler la question ... mais la façon dont j'ai formulé la question J'ai besoin d'une réponse sans curseur, donc c'est comme la meilleure approche. S'il vous plaît commenter si ce n'est pas l'étiquette correcte.

+0

+1 pour l'affichage de DDL, je n'aurais pas répondu sans elle. – RedFilter

+0

@RedFilter - merci beaucoup, je tiens à publier DDL car je ne peux pas vraiment se permettre le Bounty. :) – richaux

Répondre

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 
+0

Je recommanderais de lancer datetime pour flotter au lieu de int, comme int rounds automatiquement et peut produire des résultats indésirables. – Shagglez

+0

@Shagglez - on a l'impression que la distribution 'int' serait sûre dans ce cas, car les dates n'auraient jamais l'élément time spécifié (je devrais vraiment utiliser le type de données' date', plutôt que 'datetime'). – richaux

+0

@Shagglez vous ne devriez jamais envisager d'utiliser float ina calulation sauf si vous voulez des erreurs d'arrondi. Si vous avez besoin de décimales, spécifiez. – HLGEM

1

Pourquoi utiliser un CTE?

declare @EndDate datetime 
set @EndDate = '202' 

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 

S'il y a des actifs qui ne disposent pas d'une entrée initiale identique à la date de début de votre gamme, la requête est un peu plus complexe (mais pas trop mal)

(La jointure à la table une troisième fois (t3) et la vérification nulle est de s'assurer que les lignes correspondantes entre t1 et t2 sont consécutives)

+0

bon point concernant CTE. Je voulais juste dire basé sur l'ensemble plutôt que sur le curseur. Je vais modifier la question pour clarifier. – richaux