【问题标题】:How Do I Calculate A Running Total That Factors Both Date and Time?如何计算同时考虑日期和时间的运行总计?
【发布时间】:2016-04-26 20:47:37
【问题描述】:

我被要求创建一个存储过程来显示可变日期范围内的租赁项目数量。我有一个具有以下架构的表:

         --Note that this is condensed, and in reality has proper    constraints
        --and more columns. Many dates from this table are tied to a single
        --ContractDetail (separate table) by ContractDetailId.
        CREATE TABLE RentalContractDates
        (
            RentalDateId               INT IDENTITY(1,1)    NOT NULL, --PK
            ContractDetailId           INT                  NOT NULL, --FK
            RentalDate                 DATETIME             NOT NULL,
            Quantity                   DECIMAL(20,8)        NULL
        );

        INSERT INTO RentalContractDates (ContractDetailId, RentalDate, Quantity)
        VALUES (1, '04/01/2016 3:00 PM', 10), 
               (1, '04/10/2016 1:00 PM', 2), 
               (1, '04/15/2016 11:00 AM', -5),
               (1, '04/15/2016 11:30 AM', -2), 
               (1, '04/27/2016 2:00 PM', -5);       

用户将输入要搜索的日期范围,该过程应找到该范围内的所有日期,然后还要考虑截止时间,即客户将被收取另一天的租金。

示例场景:全球截止时间设置为下午 12:00。我在 2016 年 4 月 1 日下午 3:00 租了 10 个小部件。这基本上我的意思是我实际上是在 2016 年 4 月 2 日租了它们,因为它已经过了 04/01 的截止时间。我在 2016 年 4 月 10 日下午 1:00 再租了 2 个,所以基本上是 2016 年 4 月 11 日。我在 2016 年 4 月 15 日上午 11:00 返还 5 个小部件,在上午 11:30 返还 2 个。我想在 2016 年 4 月 27 日归还所有小部件,但我在截止时间 12:00 PM 之后到达,因此我实际上将在 2016 年 4 月 2 日至 27 日收取费用,而不是收取费用-4/28。

重要提示:如果我之前在 04/01 之前租过的数量是报告范围的开始,我需要将这些数量包含在报告中。例如,如果我在 3 月 31 日、4 月 1 日及以后有 12 次出租,那么他们的总数将增加 12 次。换句话说,任何先前的数量都需要计算为输入报告@BeginDate 和@EndDate 参数的总和。 所以 04/01 会读取 12,04/02 会读取 22,依此类推。

如您所见,我不需要用户每天输入他们的租金,我只是让他们为他们的租金设置一个开始日期和时间,并在下次他们输入日期/时间组合时被重新总结。

当前代码:我想将此查询与整个月的日历日期列表结合起来,并相应地设置它们的数量。

        DECLARE @BeginDate DATETIME = '04/01/2016',
                @EndDate DATETIME = '04/28/2016';

        DECLARE
                @CutoffTime TIME = '12:00 PM';

        SET @BeginDate = @BeginDate + @CutoffTime;
        SET @EndDate = @EndDate + @CutoffTime;

        SELECT  gbd.ContractDetailId,
                gbd.RentalDate,
                gbd.Cutoff,
                gbd.Quantity,
                'Running Total' = SUM(Quantity) OVER (PARTITION BY ContractDetailId, RentalDate, Cutoff ORDER BY RentalDate)
        FROM    (

    SELECT  
            r.ContractDetailId,
            'RentalDate' = CONVERT(Date, RentalDate),
            r2.Cutoff,
            r.Quantity
    FROM    RentalContractDates r
    INNER JOIN
        (
            SELECT 
                rcd.ContractDetailId,
                'Cutoff' = CASE WHEN CONVERT(TIME, RentalDate) >= @CutoffTime THEN 'AFTER CUTOFF' ELSE 'BEFORE CUTOFF' END
            FROM 
                RentalContractDates rcd
         ) r2
        ON r2.ContractDetailId = r.ContractDetailId
    WHERE  
            r.RentalDate Between @BeginDate and @EndDate
    GROUP BY r.ContractDetailId, CONVERT(DATE, RentalDate), r2.Cutoff, Quantity
        ) gbd
        ORDER BY RentalDate, Cutoff DESC

我想用这个 CTE 加入数据并为每个日期设置一个数量:

        ;WITH T([Date]) AS
    ( 
        SELECT @StartDate
        UNION ALL
        SELECT DATEADD(DAY,1,T.[Date]) FROM T WHERE T.[Date] < @EndDate
    )
    SELECT * FROM T

预期的最终输出: 完成后,报告最终会看起来像这样,尽管它会被旋转并在名称中包含星期几:

    ContractDetailId    RentalDate      Quantity
      ----------------------------------------------------------------
        1               04/01/2016        0  -- 0, because rentals were input after cutoff.
        1               04/02/2016        10
        1               04/03/2016        10 -- Continues until 4/10
        1               04/10/2016        10
        1               04/11/2016        12 -- Continues until 4/15
        1               04/15/2016        5  -- I returned 5 and then 2, so this should sum since both were before the cutoff time.
                                             -- Continues until 4/27.
        1               04/27/2016        5  -- 5, because -5 was entered past cutoff on 4/27.
        1               04/28/2016        0                   

我已经完成了旋转代码以及最终输出所需的动态 sql(如果需要,我可以发布此内容),但我不知道如何通过预截止/截止后正确分组这些数据并更改相应的一天。我应该如何处理这种情况?感谢您的任何建议/帮助!

编辑 1: 修正了不正确的样本数据。

【问题讨论】:

  • 您的示例数据似乎显示了 12 个租用的小部件和 14 个返回的小部件。虽然这可能是一种有利可图的商业模式,但似乎有点偏离。
  • 要处理截止时间,只需从每个原始 datetime 中减去 12*60=720 分钟,然后忽略时间部分:CAST(DATEADD(minute, -720, RentalDate) AS date)
  • @HABO 你是对的,不知道我是怎么错过的。 04/15 应该是 5。我编辑了它。

标签: tsql sum sql-server-2014-express


【解决方案1】:
  --Inputs for your function
    DECLARE @BeginDate DATE = '04/01/2016',
            @EndDate DATE = '04/28/2016',
            @ContractDetailID INT = 1;

    --Defined in the function
    DECLARE @CutoffTime TIME = '12:00 PM';

    DECLARE @PriorSum DECIMAL(20,8) = 0;

    DECLARE @RowCount INT = DATEDIFF(dd,@BeginDate,@Enddate) +1;

    --Get Any quantities before Begin Date
    SELECT @PriorSum=COALESCE(SUM(rcd.Quantity),0)
    from RentalContractDates rcd
    WHERE CAST(CASE when CAST(rcd.RentalDate as TIME) > @CutoffTime THEN DATEADD(dd,1,rcd.RentalDate) ELSE rcd.RentalDate end as date) < @BeginDate
        AND @ContractDetailID = rcd.ContractDetailId

    --Create the Days for the report
    ;WITH RecursiveRowGenerator (Row#, Iteration) AS (
           SELECT 1, 1
            UNION ALL
           SELECT Row# + Iteration, Iteration * 2
             FROM RecursiveRowGenerator
            WHERE Iteration * 2 < CEILING(SQRT(@RowCount+1))
            UNION ALL
           SELECT Row# + (Iteration * 2), Iteration * 2
             FROM RecursiveRowGenerator
            WHERE Iteration * 2 < CEILING(SQRT(@RowCount+1))
         )
         , SqrtNRows AS (
           SELECT *
             FROM RecursiveRowGenerator
            UNION ALL
           SELECT 0, 0
         )
         , Rowtbl as (  
           SELECT top (@RowCount+1) A.Row# * POWER(2,CEILING(LOG(SQRT(@RowCount+1))/LOG(2))) + B.Row# as RowNum
           FROM SqrtNRows A, SqrtNRows B
           ORDER BY A.Row#, B.Row#
         )  
      ,
    DateTable as (

      select top (@RowCount)  DATEADD(dd,RowNum,@BeginDate) AS ReportDate
      from Rowtbl
      where RowNum <= @RowCount  

      )
    ,
    --Merge the days for the report with the actual rental data
    GBD AS 
    ( SELECT  
               @ContractDetailID as ContractDetailID,
                DT.ReportDate AS 'RentalDate',
                CASE when CAST(rcd.RentalDate as TIME) > @CutoffTime THEN 'AFTER CUTOFF' ELSE 'BEFORE CUTOFF' END AS 'Cutoff',
                COALESCE(rcd.Quantity,0) AS Quantity
        FROM    DateTable DT 
        LEFT JOIN RentalContractDates rcd on 
                DT.ReportDate = CAST( CASE when CAST(rcd.RentalDate as TIME) > @CutoffTime THEN DATEADD(dd,1,rcd.RentalDate) ELSE rcd.RentalDate END as DATE) 
                AND @ContractDetailID = rcd.ContractDetailId
        WHERE DT.ReportDate Between @BeginDate and @EndDate 


    )
--Final Select    
SELECT  gbd1.ContractDetailId,
                gbd1.RentalDate,
                (select SUM(gbd2.Quantity) from GBD GBD2 where GBD1.rentaldate >= GBD2.RentalDate) + @PriorSum AS RunningTotal               
        FROM   GBD gbd1     
        GROUP BY gbd1.ContractDetailId,gbd1.RentalDate
        ORDER BY gbd1.RentalDate asc   

编辑: 递归行生成器由 Aaron Friel 编写,是我生成行 t-sql select get all Months within a range of years 的首选解决方案。它为所有日期生成行,因此如果该日期没有记录,我们可以左加入一个数量或 0。使用此生成器生成 2000 年到 2016 年的日期非常便宜。将这些日期的所有数量和之前的总和加入到这些日期是有点贵的。将它们聚合起来,即使对于在 RentalContractDates 中没有记录的日期,您也有一个运行总计是昂贵的部分。

处理截止时间问题的部分是:

gbd.RentalDate = CAST(CASE WHEN CAST(rcd.RentalDate AS TIME) > @CutoffTime THEN DATEADD(dd, 1, rcd.RentalDate)
            ELSE rcd.RentalDate END AS DATE)

它将rentaldate 仅转换为时间,与cutoffTime 进行比较,如果过去则添加一天,然后仅转换为日期。

【讨论】:

  • 我测试了您的代码,虽然它可以正常工作,但在将日期范围扩大到一年或更长时间时,它的速度会大大降低。开始日期从 2000 年开始时,处理时间超过 1 分钟。问题是我可以有很多 ContractDetailIds (100+),每个都有很多日期,我将运行报告的单个合同。我正在修改它以查看是否可以采用 GBD 合并选择并将其移动到临时表以加快速度。我相信 CTE 的递归和这个选择会减慢速度。完成后我会更新我的原始帖子。谢谢!
  • 完全同意。我应该更清楚的是,这个解决方案并不意味着在更大的日期范围内表现出色。我认为你的方法很好。
猜你喜欢
  • 1970-01-01
  • 2012-02-18
  • 1970-01-01
  • 2014-03-02
  • 2021-10-17
  • 1970-01-01
  • 2017-12-18
  • 2013-03-25
  • 1970-01-01
相关资源
最近更新 更多