【问题标题】:Get max bookings count in range获取范围内的最大预订数
【发布时间】:2015-10-08 21:06:42
【问题描述】:

我有一个ParkingLot 模型。停车场有许多可用的lots。然后,用户可以预订一个或多天的停车场。因此我有一个Booking 模型。

class ParkingLot
  has_many :bookings
end

class Booking
  belongs_to :parking_lot
end

简化用例

停车场

给定一个有 5 个可用车位的停车场:

预订

  • Bob 从周一到周日预订了一个地方
  • Sue 在周一、周三和周五各进行一次预订
  • Henry 只在星期五读书。
  • 由于周末很忙,另外 4 人预订了周六至周日。

编辑

预订有一个start_date 和一个end_date,所以Bob 的预订只有一个 条目。 Mon-Sun。 另一方面,Sue 确实有三个预订,都在同一天开始和结束。 Mon-Mon, Wed-Wed, Fri-Fri.

这为我们提供了以下预订数据:

为简单起见,我将使用首字母 (B) 和工作日 (Mon),而不是用户 ID (1) 和日期 (2015-5-15)。

 ––––––––––––––––––––––––––––––––––––––––––
| id | user_id | start_date| end_date| ... |
|––––––––––––––––––––––––––––––––––––––––––|
|  1 |    B    |    Mon    |   Sun   | ... |
|––––––––––––––––––––––––––––––––––––––––––|
|  2 |    S    |    Mon    |   Mon   | ... |
|  3 |    S    |    Wed    |   Wed   | ... |
|  4 |    S    |    Fri    |   Fri   | ... |
|––––––––––––––––––––––––––––––––––––––––––|
|  5 |    H    |    Fri    |   Fri   | ... |
|––––––––––––––––––––––––––––––––––––––––––|
|  6 |    W    |    Sat    |   Sun   | ... |
|  7 |    X    |    Sat    |   Sun   | ... |
|  8 |    Y    |    Sat    |   Sun   | ... |
|  9 |    Z    |    Sat    |   Sun   | ... |
 ––––––––––––––––––––––––––––––––––––––––––

这给了我们接下来的一周:

 –––––––––––––––––––––––––––––––––––––––––
| Mon | Tue | Wed | Thu | Fri | Sat | Sun |
|–––––––––––––––––––––––––––––––––––––––––|
|  B  |  B  |  B  |  B  |  B  |  B  |  B  |
|–––––––––––––––––––––––––––––––––––––––––|
|  S  |  -  |  S  |  -  |  S  |  -  |  -  |
|–––––––––––––––––––––––––––––––––––––––––|
|  -  |  -  |  -  |  -  |  H  |  -  |  -  |
|–––––––––––––––––––––––––––––––––––––––––|
|  -  |  -  |  -  |  -  |  -  |  W  |  W  |
|  -  |  -  |  -  |  -  |  -  |  X  |  X  |
|  -  |  -  |  -  |  -  |  -  |  Y  |  Y  |
|  -  |  -  |  -  |  -  |  -  |  Z  |  Z  |
|=========================================|
|  2  |  1  |  2  |  1  |  3  |  5  |  5  | # Bookings Count
|=========================================|
|  3  |  4  |  3  |  4  |  2  |  0  |  0  | # Available lots
 –––––––––––––––––––––––––––––––––––––––––

这些预订已经在数据库中,因此当用户想要在周一至周五进行预订时,还有空间可以这样做。但是当他想从周一到周六预订时,这是不可能的。

我的目标是查询给定时间范围内的最大预订数。最终导致可用地段

# Mon - Thursday => max bookings: 2 => 3 available lots
# Mon - Friday => max bookings: 3 => 2 available lots
# Mon - Sunday => max bookings: 5 => 0 available lots

我的一个简单但错误的方法是获取给定时间范围内的所有预订:

scope :in_range, ->(range) { where("end_date >= ?", range.first).where("start_date <= ?", range.last) }

但这绝不是正确的。从星期一到星期五的查询返回 5 个预订,一个来自 Bob,一个来自 Henry,三个来自 Sue。这会错误地假设停车场已满。


如何创建这样的查询来获取给定时间范围内的最大预订数?

这也可以是纯SQL,我很乐意稍后翻译成AR

【问题讨论】:

    标签: sql ruby-on-rails activerecord date-range


    【解决方案1】:

    有一种使用日历表的简单方法。如果您还没有,您应该创建它,它有多种用途。

    select
       c.calendar_date
       ,count(b.start_date) -- number of occupied lots
    from calendar as c
    left join bookings as b -- need left join to get dates where no lot is already booked
      on c.calendar_date between b.start_date and b.end_date
    
    -- restrict to the searched range of dates
    where calendar_date between date '2015-05-10' and date '2015-05-18'
    group by c.calendar_date
    order by c.calendar_date
    

    编辑: Vladimir Baranov 建议添加有关如何创建和使用日历表的链接。当然,实际的实现总是特定于用户和 DBMS(例如MS SQL Server),因此搜索 "calendar table" + yourDBMS 可能会显示您系统的一些源代码。

    事实上,创建日历表最简单的方法是在电子表格中计算您需要的年份范围(Excel 等,使用您需要的所有功能,如复活节计算),然后将其推送到数据库,这是一次性操作:-)


    Rails 用例¹

    首先,创建CalendarDay 模型。除了day,我还添加了更多的列,这可能会在未来的场景中派上用场。

    db/migrate/201505XXXXXX_create_calendar_days.rb

    class CreateCalendarDays < ActiveRecord::Migration
      def change
        create_table :calendar_days, id: false do |t|
          t.date :day, null: false
          t.integer :year, null: false
          t.integer :month, null: false
          t.integer :day_of_month, null: false
          t.integer :day_of_week, null: false
          t.integer :quarter, null: false
          t.boolean :week_day, null: false
        end
    
        execute "ALTER TABLE calendar_days ADD PRIMARY KEY (day)"
      end
    end
    

    然后,在运行 rake db:migrate 之后添加一个 rake 任务来填充您的模型:

    lib/tasks/calendar_days.rake

    namespace :calendar_days do
      task populate: :environment do
        (Date.new(2010,1,1)...Date.new(2049,12,31)).each do |d|
          CalendarDay.create(
            day:          d,
            year:         d.year,
            month:        d.month,
            day_of_month: d.day,
            day_of_week:  d.wday,
            quarter:      (d.month / 4) + 1,
            week_day:     ![0,6].include?(d.wday)
          )
        end
      end
    end
    

    然后运行calendar_days:populate

    最后,您可以使用 Activerecord 执行上述复杂查询

    CalendarDay.select("calendar_days.day, count(b.departure_time)")
               .joins("LEFT JOIN bookings as b on calendar_days.day BETWEEN b.departure_time and b.arrival_time")
               .where(:day => start_date..end_date)
               .group(:day)
               .order(:day)
    
    # => SELECT "calendar_days"."day", count(b.departure_time)
    #    FROM "calendar_days"
    #    LEFT JOIN bookings as b on calendar_days.day BETWEEN b.departure_time and b.arrival_time
    #    WHERE ("calendar_days"."day" BETWEEN '2015-05-04 13:41:44.877338' AND '2015-05-11 13:42:00.076805')
    #    GROUP BY day  
    #    ORDER BY "calendar_days"."day" ASC
    

    1 - TheChamp添加的用例

    【讨论】:

    • 非常感谢@dnoeth,这是我追求的“开箱即用的思维”!我可以想象很多其他场景可以派上用场。我会等待几天的赏金,但我想你会得到它:)
    • @TheChamp:这不是“开箱即用”,而是简单的 SQL 思维 :-)
    • 对你来说不是。对我来说,我从来没有想过这个^^
    • @dnoeth,在答案中添加一些关于table of numberscalendar table 的链接,以及你如何generate one,这将是完美的。
    【解决方案2】:

    我将为天数分配一些数值来简化这个问题。您可以将其设为“id”列(或类似内容)。

    Mon-1、Tue-2、Wed-3、Thu-4、Fri-5、Sat-6、Sun-7

    现在这是一个非常简单的 SQL:

    select 5-max(bookings) where id >= 1 and id <= 4
    

    这是周一至周四,您可以不断更改范围以获取特定时段之间的可用手数。 Bob 的需求可以代替值 1 和 4,他会知道是否有可用的批次。

    我假设您要将其应用于大规模解决方案,并且如果您实际使用日期,则可以很容易地调整上述查询以每次都获得正确的值。

    【讨论】:

    • 嗨 CodeNewbie,我想我的问题有点不清楚。我更新了一些内容,感谢您的时间!
    【解决方案3】:

    您需要按天分组,因为您的预订是按天计算的。根据您的总地段检查特定日期的总预订量,您可以获得当天的可用空间。

    让我们创建一个包含以下条目的预订表:

       Book_Date      Slot_Id    Customer_Id
       2015-05-14      1          100
       2015-05-14      2          200
       2015-05-14      3          400
       2015-05-15      1          100
       2015-05-16      1          100
       2015-05-17      1          100
    

    做这个查询:

      SELECT book_date , count(*) AS booked, 5- count(*) AS lot_available 
      FROM bookings 
      WHERE bookdate>='2015-05-14' AND book_date<'2015-05-21' 
      GROUP BY book_date
    

    会给你一些东西:

       book_date booked   lot_available
       2015-05-14     3    2
       2015-05-15     1    4 
       2015-05-16     1    4 
       2015-05-17     1    4 
    

    您现在知道每天有多少可用批次。

    有一个问题需要解决,如果某天没有预订,则不会在上面的结果中列出,你需要添加一个日历表或构建一个小临时表来解决。

    使用它来生成未来 7 天的表格:

    SELECT DATE_ADD(CURDATE(), INTERVAL 1 DAY) nday 
    UNION 
    SELECT DATE_ADD(CURDATE(), INTERVAL 2 DAY) nday 
    UNION 
    SELECT DATE_ADD(CURDATE(), INTERVAL 3 DAY) nday 
    UNION 
    SELECT DATE_ADD(CURDATE(), INTERVAL 4 DAY) nday 
    UNION 
    SELECT DATE_ADD(CURDATE(), INTERVAL 5 DAY) nday 
    UNION 
    SELECT DATE_ADD(CURDATE(), INTERVAL 6 DAY) nday 
    UNION 
    SELECT DATE_ADD(CURDATE(), INTERVAL 7 DAY) nday 
    

    并检查您的代码

      SELECT nday AS book_date , count(lot_id) AS booked, 5- count(lot_id) AS lot_available 
      FROM (SELECT DATE_ADD(CURDATE(), INTERVAL 1 DAY) nday 
    UNION 
    SELECT DATE_ADD(CURDATE(), INTERVAL 2 DAY) AS nday 
    UNION 
    SELECT DATE_ADD(CURDATE(), INTERVAL 3 DAY)  
    UNION 
    SELECT DATE_ADD(CURDATE(), INTERVAL 4 DAY)  
    UNION 
    SELECT DATE_ADD(CURDATE(), INTERVAL 5 DAY)  
    UNION 
    SELECT DATE_ADD(CURDATE(), INTERVAL 6 DAY)  
    UNION 
    SELECT DATE_ADD(CURDATE(), INTERVAL 7 DAY)  ) days
    LEFT JOIN bookings 
    ON days.nday = books.book_date
    GROUP by nday
    

    它会给你类似的东西:

    +------------+--------+---------------+
    | book_date  | booked | lot_available |
    +------------+--------+---------------+
    | 2015-05-15 |      2 |             3 |
    | 2015-05-16 |      0 |             5 |
    | 2015-05-17 |      0 |             5 |
    | 2015-05-18 |      0 |             5 |
    | 2015-05-19 |      0 |             5 |
    | 2015-05-20 |      0 |             5 |
    | 2015-05-21 |      0 |             5 |
    

    (最后的结果是使用不同的样本数据生成的)

    这是修改版:

    SELECT SUM(CASE WHEN lot IS NULL THEN 0 ELSE 1 END) AS booked, nday
    FROM (
      SELECT  lot, c.nday  FROM  (   
        SELECT DATE_ADD(CURDATE(), INTERVAL 1 DAY) AS nday    
        UNION    SELECT DATE_ADD(CURDATE(), INTERVAL 2 DAY)    
        UNION    SELECT DATE_ADD(CURDATE(), INTERVAL 3 DAY)     
        UNION    SELECT DATE_ADD(CURDATE(), INTERVAL 4 DAY)     
        UNION    SELECT DATE_ADD(CURDATE(), INTERVAL 5 DAY)     
        UNION    SELECT DATE_ADD(CURDATE(), INTERVAL 6 DAY)     
        UNION    SELECT DATE_ADD(CURDATE(), INTERVAL 7 DAY)   
        ) c 
        LEFT JOIN bookings l 
          ON l.book_date<=c.nday AND l.end_date >=c.nday 
        ) e
    GROUP BY NDAY
    

    为您提供如下结果:

    +--------+------------+
    | booked | nday       |
    +--------+------------+
    |      5 | 2015-05-16 |
    |      5 | 2015-05-17 |
    |      2 | 2015-05-18 |
    |      0 | 2015-05-19 |
    |      0 | 2015-05-20 |
    |      0 | 2015-05-21 |
    |      0 | 2015-05-22 |
    +--------+------------+
    

    【讨论】:

    • GROUP BY day 是一个很好的提示/起点。你会用什么列来实现这个?它还会考虑预订开始和结束之间的所有天数吗?以鲍勃为例。如果您进一步扩展答案,那就太好了
    • 嗨蒂姆,非常感谢您的回复!刚刚看到你是如何编辑它的,男孩,大量的工作投入其中!谢谢!我解释得不够好,是预订有start date & 和end date,这使事情更加复杂。我希望这很清楚。由于这很复杂,我将开始赏金,如果您设法回答,我很乐意奖励您,而不仅仅是赞成和接受:)
    • 有两个日期列是有意义的。贴一些真实数据。你真的在使用 Mon.开始日期?
    • 不,如前所述,它是简化的。日期类似于2015-05-21
    • 我修改了代码。不需要额外的信用。确保彻底测试。
    【解决方案4】:

    我认为这可以通过修改后的数据模型来简化

    为了使示例更清晰,我将类的名称更改为更具描述性。

    我们将使用类:

    Lot, Space, and Reservation
    

    关系应该是

    Lot    
      has_many :spaces    
    
    Space  
        belongs_to :lot   
        has_many :reservations
    
    Reservations   
        belongs_to: :space   
        reservation_date #not a range just a date
    

    然后你可以按照以下方式做一些事情:

    dates_for_search = [reservation_date1, reservation_date2, reservation_date3]
    
    open_spaces_and_date = []
    dates_for_search.each do |date|
       @lot.spaces.each do |space|
         if space.reservations.bsearch{|res| res.reservation_date == date} == nil 
           open_spaces_and_date << {:space => space; :date => date}
         end
       end
    
    #now open_spaces_and_dates will have a list of availability. 
    end
    

    【讨论】:

    • 只定义一个预订日期会限制整个应用程序及其功能。用户必须预订一个时间范围。如果有人想停车整整一周,他不应该创建 7 个预订。仍然感谢您的尝试。
    • 不,用户不应该,但您的应用程序应该。我将更新示例以显示其余的交互。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-12-03
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多