获取最大预订数量

时间:2015-05-14 12:11:49

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

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

class ParkingLot
  has_many :bookings
end

class Booking
  belongs_to :parking_lot
end

简化用例

ParkingLot

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

预订

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

修改

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

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

为简单起见,而不是user_id(1)&日期(2015-5-15),我将使用初始(B)和工作日(Mon)。

 ––––––––––––––––––––––––––––––––––––––––––
| 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个预订,一个来自鲍勃,一个来自亨利,三个来自苏。这将错误地假设停车场已满。

如何创建此类查询以获取给定时间范围内的最大预订数量?

这也可以是纯SQL,我很乐意将其转换为AR。{/ p>

4 个答案:

答案 0 :(得分:4)

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

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,这对于未来的场景可能会派上用场。

<强>分贝/迁移/ 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

添加的用例

答案 1 :(得分:0)

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

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

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

答案 2 :(得分:0)

我将为日期分配一些数值以简化此问题。你可以把这个作为“id”&#39;专栏(或类似的东西)。

  

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,并且他将知道是否有可用的批次。

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

答案 3 :(得分:0)

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

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

我们将使用类:

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