2016-04-26 111 views
1

我被要求创建一个存储过程来显示可变日期范围内的租赁项目数量。我有以下架构表:如何计算日期和时间因素的运行总数?

  --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 PM。我在2016年4月1日下午3点租用了10个小部件。这基本上意味着我实际上在4/02/2016租用了它们,因为它已经超过了04/01的截止时间。我在2016年4月10日下午1:00再租2个,基本上2016年4月11日。我于2016年4月15日上午11:00返回5个小工具,上午11:30还有2个小工具。我想在2016年4月27日归还所有小部件,但是我抵达中午12点的截止时间,因此,而不是4/02-4/27收取费用,实际上我将收取4/02 -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已经完成(如果需要,我可以发布这个动态SQL),但是我很遗憾如何通过截止前/截止后正确分组这些数据并相应地更改日期。我应该如何处理这种情况?感谢您的任何建议/帮助!

编辑1:修复不正确的采样数据。

+1

您的示例数据似乎显示12个小工具出租和14个返回。虽然这可能是一个有利可图的商业模式,但它似乎有点关闭。 – HABO

+0

要处理截止时间,只需从每个原始的'datetime'中减去'12 * 60 = 720'分钟,然后忽略时间部分:'CAST(DATEADD(minute,-720,RentalDate)AS date)'。 –

+0

@HABO你说得对,不知道我是如何错过的。 04/15应该是5.我编辑它。 –

回答

0
--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弗里尔,是我转到解决方案,用于生成行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,增加了一天,如果过去的,然后转换为只日期。

+0

我测试了你的代码,当它正常工作时,它扩展日期范围时说,一年或更长时间会大大减慢。从2000年开始的开始日期,处理需要1分多钟。问题是,我可以有很多ContractDetailIds(100+),每个都有很多日期,我将运行该报告的单个合同。我正在修改它以查看是否可以将GBD合并选择并将其移动到临时表以加快速度。我相信CTE和这个选择的递归是减慢速度的原因。完成后,我会更新原始帖子。谢谢! –

+0

完全同意。我应该更清楚的认识到,这个解决方案并不意味着要在更大的日期范围内执行。我认为你的方法很好。 – TheWildMan