2010-01-10 147 views
2

假设我有一个奖励SQL表,其中包含Date和Amount字段。我需要生成一系列连续日期,每天奖励金额和运行(累计)总额的表格。改进SQL查询:随着时间推移的累计金额

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 

这个SQL工作,但没有那么快,我想:

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) 

我尝试添加一个索引Awards.Date,但它使一个很小的差别。

在我使用缓存等其他策略之前,有没有更有效的方法来编写运行总计算?

回答

3

我一般使用临时表这样的:

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 

如果你不能使日期列主键,那么你需要包括在INSERT语句中ORDER BY [Date]

此外,这个问题以前曾被问过几次。请参阅here或搜索“sql running total”。据我所知,我发布的解决方案仍然是性能最佳的解决方案,也易于编写。

+0

非常感谢 - 这是完美的。惊人的速度如此之快。 – 2010-01-10 18:57:04

+0

我以前从来没有见过类似“SET @x = a = @x + b”的语法。为什么RDBMS能做到这一点? – MatBailie 2010-01-10 19:03:17

+0

@Dems:此语法适用于SQL Server,但您可以在mysql中执行一些非常类似的操作(请参阅我的答案底部附近的链接 - 某人发布了mysql版本的位置)。 – Aaronaught 2010-01-10 19:26:23

0

我没有在我面前的数据库设置,所以我希望下面的作品第一枪。像这样的模式应该导致多更快的查询......你刚刚加盟的两倍,聚集同类量:

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 

注:我改变了你的表表达式往上顶,有人离开了第一天之前......如果这是故意的,只需要在这条语句上打一个where子句就可以排除它。如果这不起作用或不适合,请在评论中告诉我,我会做出任何调整。

+0

第一天的事情很好 - 在发布后不久发生了同样的变化。实际上,我尝试了按照您之前在此建议的方式进行更改(使用组而不是内联查询,而使用一个CTE而不是两个),并且令人惊讶的是他们没有太大的性能影响。 @Aaronaught发布的技术比我尝试过的其他技术快4倍 - 我认为主要是因为临时表上的日期主键。 – 2010-01-10 20:43:34

0

这是基于@Aaronaught的答案的工作解决方案。我必须在T-SQL中克服的唯一问题是@RunningTotal等不能为空(需要转换为零)。

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