【问题标题】:Calculate Price For Overlapping Date Range计算重叠日期范围的价格
【发布时间】:2015-05-20 13:57:58
【问题描述】:

我有一个名为 HotelRate 的简单表

HID  |  START_DATE  |   END_DATE    |   PRICE_PER_DAY
--------------------------------------
1        01/1/2015       10/1/2015       100
1        11/1/2015       20/1/2015       75
1        21/1/2015       30/1/2015       110

如果用户查询5/1/201525/1/2015 之间的总价格,计算酒店房间价格的最简单方法是什么。

我已经检查过:

但这对我来说没有多大意义。

我尝试了几个查询,但这些查询似乎是在盲目地击中箭头。有人可以建议我一个简单而优雅的方法吗?


@JamesZ

在运行第一个查询时我得到

start_date end_date   duration    price_per_day
---------- ---------- ----------- -------------
2015-01-01 2015-01-10 5           100
2015-01-11 2015-01-20 9           75
2015-01-21 2015-01-30 4           110

第一个范围5 可以,第二个范围应该是10,第三个应该是5

如何计算天数:startend 日期之间的总夜数,与天数差异相同

05-Jan-15   06-Jan-15   1 Night
06-Jan-15   07-Jan-15   1 Night
07-Jan-15   08-Jan-15   1 Night
08-Jan-15   09-Jan-15   1 Night
09-Jan-15   10-Jan-15   1 Night
10-Jan-15   11-Jan-15   1 Night
11-Jan-15   12-Jan-15   1 Night
12-Jan-15   13-Jan-15   1 Night
13-Jan-15   14-Jan-15   1 Night
14-Jan-15   15-Jan-15   1 Night
15-Jan-15   16-Jan-15   1 Night
16-Jan-15   17-Jan-15   1 Night
17-Jan-15   18-Jan-15   1 Night
18-Jan-15   19-Jan-15   1 Night
19-Jan-15   20-Jan-15   1 Night
20-Jan-15   21-Jan-15   1 Night
21-Jan-15   22-Jan-15   1 Night
22-Jan-15   23-Jan-15   1 Night
23-Jan-15   24-Jan-15   1 Night
24-Jan-15   25-Jan-15   1 Night
               Count : 20 Night

【问题讨论】:

  • 这是 MySQL 还是 MS SQL Server?
  • @Mureinik Sql Server

标签: sql sql-server


【解决方案1】:

这样的事情应该可以解决问题:

declare @startdate date, @enddate date

set @startdate = '20150105'
set @enddate = '20150125'

select
  start_date,
  end_date,
  datediff(
    day, 
    case when @startdate > start_date then @startdate else start_date end, 
    case when @enddate < end_date then @enddate else end_date end) as duration,
  price_per_day
from
  reservation
where
  end_date >= @startdate and
  start_date <= @enddate

这只是处理带有大小写的重叠范围,因此如果预订开始是正确的,则使用它,否则搜索条件和结束日期相同。此处的天数和价格是分开的,但您可以将它们相乘得到结果。

SQL 小提琴:http://sqlfiddle.com/#!3/4027b3/1

编辑,这样得到总和:

declare @startdate date, @enddate date

set @startdate = '20150105'
set @enddate = '20150125'

select
  sum(datediff(
    day, 
    case when @startdate > start_date then @startdate else start_date end, 
    case when @enddate < end_date then @enddate else end_date end)  
  * price_per_day)
from
  reservation
where
  end_date >= @startdate and
  start_date <= @enddate

【讨论】:

  • 我故意省略了它,因为这样可以更好地了解 SQL 的作用。要获得一行的总和,只需在 sql 中使用 sum(datediff(...) * price_per_day)。
  • 不是减1吗?天数是不包含在内的,所以我认为您需要在持续时间上加 1 以使其包含在内。
  • @JamesZ 这看起来很棒,开始和结束日期似乎是排他性的,就我而言,它需要包含在内!
  • @PaRiMaLRaJ 对不起,我的错。固定范围
  • @JamesZ 最后一节的持续时间不应该是 5 吗?
【解决方案2】:

您将需要一个日历表,但每个数据库都应该有一个。 实际实现总是特定于用户和 DBMS(例如MS SQL Server),因此搜索“日历表”+ yourDBMS 可能会显示您系统的一些源代码。

select HID, sum(PRICE_PER_DAY)
from calendar_table as c
join HotelRate
  on calendar_date between START_DATE and END_DATE
group by HID

【讨论】:

    【解决方案3】:

    如果您有可用的现有日期表,这很容易处理。还没有?您将在下面找到两个可帮助您入门的功能。这就是你使用它们的方式:

    -- Arguments can be passed in any order
    SELECT * FROM dbo.RangeDate('2015-12-31', '2015-01-01');
    SELECT * FROM dbo.RangeSmallInt(10, 0);
    
    SELECT A.HID, SUM(A.PRICE_PER_DAY)
    FROM dbo.RangeDate('2000-01-01', '2020-12-31') Calendar
    JOIN HotelRate A
        ON Calendar.D BETWEEN A.START_DATE and A.END_DATE
    GROUP BY A.HID;
    

    您可以将 RangeDate 函数用作日历,也可以使用它来构建自己的日历函数/表格。

    -- Generate a range of up to 65,536 contiguous DATES
    CREATE FUNCTION dbo.RangeDate (   
        @date1 DATE = NULL
      , @date2 DATE = NULL
    )   
    RETURNS TABLE   
    AS   
    RETURN (
        SELECT D = DATEADD(DAY, A.N, CASE WHEN @date1 <= @date2 THEN @date1 ELSE @date2 END)
        FROM dbo.RangeSmallInt(
            CASE WHEN @date1 IS NOT NULL AND @date2 IS NOT NULL THEN 0 END
          , ABS(DATEDIFF(DAY, @date1, @date2))
        ) A
    );
    
    -- Generate a range of up to 65,536 contiguous BIGINTS
    CREATE FUNCTION dbo.RangeSmallInt (
        @n1 BIGINT = NULL
      , @n2 BIGINT = NULL
    )
    RETURNS TABLE
    AS
    RETURN (
        WITH Numbers AS (
            SELECT N FROM(VALUES
                (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1) -- 16
              , (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1) -- 32
              , (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1) -- 48
              , (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1) -- 64
              , (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1) -- 80
              , (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1) -- 96
              , (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1) -- 112
              , (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1) -- 128
              , (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1) -- 144
              , (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1) -- 160
              , (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1) -- 176
              , (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1) -- 192
              , (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1) -- 208
              , (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1) -- 224
              , (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1) -- 240
              , (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1) -- 256
            ) V (N)
        )    
        SELECT TOP (
                   CASE
                       WHEN @n1 IS NOT NULL AND @n2 IS NOT NULL THEN ABS(@n2 - @n1) + 1
                       ELSE 0
                   END
               )
               N = ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) - 1 + CASE WHEN @n1 <= @n2 THEN @n1 ELSE @n2 END
        FROM Numbers A, Numbers B
        WHERE ABS(@n2 - @n1) + 1 < 65537
    );
    

    【讨论】:

      【解决方案4】:

      您可以使用它来计算每个期间的价格,然后将其总结为总成本。它使用案例语句来计算每个时期有多少天,因此在您的示例中这是 5,9 和 4:

      Declare @startdate date = '2015-01-05',
              @todate date = '2015-01-25'
      
      Select sum(price_per_period) as TotalPrice -- The cost for all periods is summed to give a total
      from
      -- First it works out the number of days in the period with a case statement and then
      -- multiplies this by the daily rate to get the total for that period
      (Select price_per_day * case when Start_date <= @startdate then DATEDIFF(day, @startdate,end_date) else  
          case when Start_date > @startdate and end_date < @todate then DATEDIFF(day, start_date,end_date) else 
              case when Start_date > @startdate and end_date >= @todate then  DATEDIFF(day, start_date, @todate) end
              end
          end price_per_period
      
       from pricetable
       where (Start_date between @Startdate and @todate) or 
            (end_date between @Startdate and @todate)
      ) a
      

      这消除了对单独日历表的需要

      SQL 小提琴:http://www.sqlfiddle.com/#!6/25e63/4/0

      【讨论】:

        【解决方案5】:

        这应该足够快,因为您首先生成日历,然后只使用联接。每家酒店的总价也可以通过分组来实现:

        数据定义:

        create table HotelRate(HID int, START_DATE date, END_DATE date, PRICE_PER_DAY int);
        
        insert into HotelRate values
        (1, '20150101', '20150110', 100),
        (1, '20150111', '20150120', 75),
        (1, '20150121', '20150130', 110),
        (2, '20150101', '20150110', 10),
        (2, '20150111', '20150120', 5),
        (2, '20150121', '20150130', 50)
        

        查询:

        declare @sd date = '20150105' , @ed date = '20150125'
        
        ;with c as(select @sd d union all select dateadd(dd, 1, d) from c where d < @ed)
        select h.HID, h.START_DATE, h.END_DATE, sum(PRICE_PER_DAY) PRICE
        from c join HotelRate h on c.d >= h.START_DATE and c.d < h.END_DATE
        group by grouping sets((h.HID, h.START_DATE, h.END_DATE),(h.HID))
        

        输出:

        HID START_DATE  END_DATE    PRICE
        1   2015-01-01  2015-01-10  500
        1   2015-01-11  2015-01-20  675
        1   2015-01-21  2015-01-30  550
        1   (null)      (null)      1725
        2   2015-01-01  2015-01-10  50
        2   2015-01-11  2015-01-20  45
        2   2015-01-21  2015-01-30  250
        2   (null)      (null)      345
        

        这可以通过计数表进一步优化。更重要的是,如果您在数据库中创建日历表,它将是即时的。

        这是小提琴http://sqlfiddle.com/#!3/25e7bc/1

        假设您创建了一些日历表Calendar(d date),其中包含从例如1900-01-01 到结束2100-01-01 的日期。在日期列的CalendarHotelRange 表上添加索引。那么上面的查询可以改写为:

        select h.HID, h.START_DATE, h.END_DATE, sum(PRICE_PER_DAY) PRICE
        from Calendar c join HotelRate h on c.d >= h.START_DATE and c.d < h.END_DATE
        where c.d between @sd and @ed
        group by grouping sets((h.HID, h.START_DATE, h.END_DATE),(h.HID))
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2017-05-08
          • 2020-02-20
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2023-03-26
          • 2023-01-19
          • 1970-01-01
          相关资源
          最近更新 更多