2010-01-10 5 views
2

Supposons que j'ai une table SQL de récompenses, avec des champs pour la date et le montant. J'ai besoin de générer une table avec une séquence de dates consécutives, le montant accordé chaque jour et le total cumulé.Améliorer la requête SQL: montants cumulés au fil du temps

Date   Amount_Total Amount_RunningTotal 
---------- ------------ ------------------- 
1/1/2010    100     100 
1/2/2010    300     400 
1/3/2010    0     400 
1/4/2010    0     400 
1/5/2010    400     800 
1/6/2010    100     900 
1/7/2010    500     1400 
1/8/2010    300     1700 

Ce SQL fonctionne, mais pas aussi vite que je le voudrais:

Declare @StartDate datetime, @EndDate datetime 
Select @StartDate=Min(Date), @EndDate=Max(Date) from Awards 

; With 

/* Returns consecutive from numbers 1 through the 
number of days for which we have data */ 
Nbrs(n) as (
    Select 1 Union All 
    Select 1+n 
    From Nbrs 
    Where n<=DateDiff(d,@StartDate,@EndDate)), 

/* Returns all dates @StartDate to @EndDate */ 
AllDays as (
    Select Date=DateAdd(d, n, @StartDate) 
    From Nbrs) 

/* Returns totals for each day */ 
Select 
d.Date, 
Amount_Total = (
     Select Sum(a.Amount) 
     From Awards a 
     Where a.Date=d.Date), 
Amount_RunningTotal = (
     Select Sum(a.Amount) 
     From Awards a 
     Where a.Date<=d.Date) 
From AllDays d 
Order by d.Date 
Option(MAXRECURSION 1000) 

J'ai essayé d'ajouter un index Awards.Date, mais il a fait une différence très minime. Avant de recourir à d'autres stratégies comme la mise en cache, existe-t-il un moyen plus efficace de coder le calcul du total cumulatif?

Répondre

3

J'utilise généralement une table temporaire pour cela:

DECLARE @Temp TABLE 
(
    [Date] date PRIMARY KEY, 
    Amount int NOT NULL, 
    RunningTotal int NULL 
) 

INSERT @Temp ([Date], Amount) 
    SELECT [Date], Amount 
    FROM ... 

DECLARE @RunningTotal int 

UPDATE @Temp 
SET @RunningTotal = RunningTotal = @RunningTotal + Amount 

SELECT * FROM @Temp 

Si vous ne pouvez pas faire la colonne date une clé primaire, vous devez inclure un ORDER BY [Date] dans la déclaration INSERT.

En outre, cette question a été posée plusieurs fois auparavant. Voir here ou rechercher "sql running total". La solution que j'ai publiée est, pour autant que je sache, celle qui offre le meilleur rendement, et qui est aussi facile à écrire.

+0

Merci beaucoup - c'est parfait. Incroyable à quelle vitesse c'est. –

+0

Je n'ai jamais vu de syntaxe comme "SET @x = a = @x + b" auparavant. Pour quel RDBMSes cela fonctionne-t-il? – MatBailie

+0

@Dems: Cette syntaxe est pour SQL Server, mais vous pouvez faire quelque chose de très similaire dans mysql (voir le lien au bas de ma réponse - où quelqu'un a posté une version mysql). – Aaronaught

0

Je n'ai pas de configuration de base de données devant moi donc j'espère que le premier fonctionne. Un modèle comme celui-ci devrait se traduire par une requête beaucoup plus rapide ... vous êtes juste se joindre à deux fois, montant similaire d'agrégation:

Declare @StartDate datetime, @EndDate datetime 
Select @StartDate=Min(Date), @EndDate=Max(Date) from Awards 
; 
WITH AllDays(Date) AS (SELECT @StartDate UNION ALL SELECT DATEADD(d, 1, Date) 
         FROM AllDays 
         WHERE Date < @EndDate) 

SELECT d.Date, sum(day.Amount) Amount_Total, sum(running.Amount) Amount_RunningTotal 
FROM AllDays d 
    LEFT JOIN (SELECT date, SUM(Amount) As Amount 
       FROM Awards 
       GROUP BY Date) day 
      ON d.Date = day.Date 
    LEFT JOIN (SELECT date, SUM(Amount) As Amount 
       FROM Awards 
       GROUP BY Date) running 
       ON (d.Date >= running.Date) 
Group by d.Date 
Order by d.Date 

Note: J'ai changé votre expression de table en haut, il quittait le premier jour avant ... si cela est intentionnel, appliquez simplement une clause where pour l'exclure. Faites-moi savoir dans les commentaires si cela ne fonctionne pas ou ne correspond pas et je ferai tous les ajustements.

+0

Bonne prise sur le premier jour - fait le même changement peu de temps après l'affichage. En fait, j'ai essayé les changements suivant les lignes que vous proposez ici (en utilisant un groupe par plutôt qu'une requête en ligne, et un CTE au lieu de deux), et étonnamment, ils n'ont pas eu beaucoup d'impact sur les performances. La technique @Aaronaught posté est environ 4x plus rapide que toute autre chose que j'ai essayé - je pense principalement à cause de la clé primaire de la date sur la table temporaire. –

0

Voici une solution de travail basée sur la réponse de @ Aaronaught. Le seul gotcha que j'ai dû surmonter dans T-SQL était que @RunningTotal etc. ne peut pas être nul (doit être converti en zéro).

Declare @StartDate datetime, @EndDate datetime 
Select @StartDate=Min(StartDate),@EndDate=Max(StartDate) from Awards 

/* @AllDays: Contains one row per date from @StartDate to @EndDate */ 
Declare @AllDays Table (
    Date datetime Primary Key) 
; With 
Nbrs(n) as (
    Select 0 Union All 
    Select 1+n from Nbrs 
    Where n<=DateDiff(d,@StartDate,@EndDate) 
    ) 
Insert into @AllDays 
Select Date=DateAdd(d, n, @StartDate) 
From Nbrs 
Option(MAXRECURSION 10000) /* Will explode if working with more than 10000 days (~27 years) */ 

/* @AmountsByDate: Contains one row per date for which we have an Award, along with the totals for that date */ 
Declare @AmountsByDate Table (
    Date datetime Primary Key, 
    Amount money) 
Insert into @AmountsByDate 
Select 
    StartDate, 
    Amount=Sum(Amount) 
from Awards a 
Group by StartDate 

/* @Result: Joins @AllDays and @AmountsByDate etc. to provide totals and running totals for every day of the award */ 
Declare @Result Table (
    Date datetime Primary Key, 
    Amount money, 
    RunningTotal money) 
Insert into @Result 
Select 
    d.Date, 
    IsNull(bt.Amount,0), 
    RunningTotal=0 
from @AllDays d 
Left Join @AmountsByDate bt on d.Date=bt.Date 
Order by d.Date 

Declare @RunningTotal money Set @RunningTotal=0 
Update @Result Set @RunningTotal = RunningTotal = @RunningTotal + Amount 

Select * from @Result 
Questions connexes