获取最大预订数量

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

 class ParkingLot has_many :bookings end class Booking belongs_to :parking_lot end 

简化的用例

停车场

鉴于有5个可用地段的停车场:

预订

  • 鲍勃从周一到周日预订了一个地方
  • 苏在周一,周三和周五分别预订一次
  • 亨利只在星期五上书。
  • 由于周末很忙,其他4人从周六到周日预订。

编辑

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

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

为简单起见,我将使用初始( B )和工作日( Mon )来代替user_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

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

 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等等,然后运行所需的所有function,如复活节计算),然后将其推送到数据库,这是一个一次性操作:-)


Rails用例¹

首先,创建CalendarDay模型。 我添加的列数多于day ,这对于未来的场景可能会派上用场。

分贝/迁移/ 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 /任务/ 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添加的用例

我将为日期分配一些数值来简化这个问题。 你可以把它变成’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,并且他将知道是否有可用的批次。

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

由于您的预订是每日预订,因此您需要按天分组。 检查特定日期的总预订量与您的总批次数,您将获得当天的可用空间。

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

  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 | +--------+------------+ 

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

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

我们将使用类:

 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