获取范围内的最大预订数
Get max bookings count in range
我有一个 ParkingLot
模型。停车场有多个可用 lots
。然后,用户可以预订一天或多天的停车场。因此我有一个 Booking
模型。
class ParkingLot
has_many :bookings
end
class Booking
belongs_to :parking_lot
end
简化用例
停车场
给定一个有 5 个可用车位的停车场:
预订
- 鲍勃预订了周一至周日的位置
- Sue 在周一、周三和周五各预订一次
- 亨利只在星期五读书。
- 由于周末比较忙,另外4个人订了周六到周日
编辑
预订有 start_date
和 end_date
,因此 Bob 的预订只有 一个 条目。 Mon-Sun
。
另一方面,Sue 确实有三个预订,都在同一天开始和结束。 Mon-Mon
, Wed-Wed
, Fri-Fri
.
这为我们提供了以下预订数据:
为简单起见,我将使用首字母 (B
) 和星期几,而不是 user_id (1
) 和日期 (2015-5-15
) (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
–––––––––––––––––––––––––––––––––––––––––
这些预订已经在数据库中,所以当 新 用户想要预订周一至周五时,space 可以这样做。但是当他想订周一到周六的时候,这就不行了。
我的目标是查询给定时间范围内的最大预订数。最终导致可用手数
# 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) }
但这绝不是正确的。从周一到周五查询 returns 5 个预订,一个来自 Bob,一个来自 Henry,三个来自 Sue。这会错误地假设停车场已满。
我如何创建这样的查询来获取给定时间范围内的最大预订数?
这也可以是纯SQL
,我很乐意将其翻译成AR
lateron
您需要按天分组,因为您的预订是按天进行的。根据您的总手数检查特定日期的总预订量,您当天有空 space。
让我们创建一个包含以下条目的 table 预订:
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
您现在知道每天有多少手可拍了。
有一个问题需要解决,如果某一天没有预订,则不会在上面的结果中列出,您需要添加一个日历table或建立一个小临时table 来解决。
使用它为接下来的 7 天生成 table:
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 |
+--------+------------+
我打算给天分配一些数值来简化这个问题。您可以将其设为 '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,他将知道是否有可用手数。
我假设您要将其应用于大规模解决方案,并且如果您实际使用日期,则可以非常轻松地调整上述查询以每次都获得正确的值。
有一种使用日历的简单方法 table。如果您还没有,您应该创建它,它有多种用途。
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 suggested to add a link on how to create and use a calendar table. Of course the actual implementation is always user and DBMS specific (e.g. MS SQL Server),因此搜索 "calendar table" + yourDBMS 可能会显示您系统的一些源代码。
事实上,创建日历的最简单方法 table 是在电子表格中计算您需要的年份范围(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
然后,在 运行ning 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
添加的用例
我认为这可以通过修改后的数据模型来简化
为了使示例更清楚,我更改了 类 的名称以使其更具描述性。
我们将使用 类:
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
我有一个 ParkingLot
模型。停车场有多个可用 lots
。然后,用户可以预订一天或多天的停车场。因此我有一个 Booking
模型。
class ParkingLot
has_many :bookings
end
class Booking
belongs_to :parking_lot
end
简化用例
停车场
给定一个有 5 个可用车位的停车场:
预订
- 鲍勃预订了周一至周日的位置
- Sue 在周一、周三和周五各预订一次
- 亨利只在星期五读书。
- 由于周末比较忙,另外4个人订了周六到周日
编辑
预订有 start_date
和 end_date
,因此 Bob 的预订只有 一个 条目。 Mon-Sun
。
另一方面,Sue 确实有三个预订,都在同一天开始和结束。 Mon-Mon
, Wed-Wed
, Fri-Fri
.
这为我们提供了以下预订数据:
为简单起见,我将使用首字母 (B
) 和星期几,而不是 user_id (1
) 和日期 (2015-5-15
) (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
–––––––––––––––––––––––––––––––––––––––––
这些预订已经在数据库中,所以当 新 用户想要预订周一至周五时,space 可以这样做。但是当他想订周一到周六的时候,这就不行了。
我的目标是查询给定时间范围内的最大预订数。最终导致可用手数
# 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) }
但这绝不是正确的。从周一到周五查询 returns 5 个预订,一个来自 Bob,一个来自 Henry,三个来自 Sue。这会错误地假设停车场已满。
我如何创建这样的查询来获取给定时间范围内的最大预订数?
这也可以是纯SQL
,我很乐意将其翻译成AR
lateron
您需要按天分组,因为您的预订是按天进行的。根据您的总手数检查特定日期的总预订量,您当天有空 space。
让我们创建一个包含以下条目的 table 预订:
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
您现在知道每天有多少手可拍了。
有一个问题需要解决,如果某一天没有预订,则不会在上面的结果中列出,您需要添加一个日历table或建立一个小临时table 来解决。
使用它为接下来的 7 天生成 table:
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 |
+--------+------------+
我打算给天分配一些数值来简化这个问题。您可以将其设为 '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,他将知道是否有可用手数。
我假设您要将其应用于大规模解决方案,并且如果您实际使用日期,则可以非常轻松地调整上述查询以每次都获得正确的值。
有一种使用日历的简单方法 table。如果您还没有,您应该创建它,它有多种用途。
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 suggested to add a link on how to create and use a calendar table. Of course the actual implementation is always user and DBMS specific (e.g. MS SQL Server),因此搜索 "calendar table" + yourDBMS 可能会显示您系统的一些源代码。
事实上,创建日历的最简单方法 table 是在电子表格中计算您需要的年份范围(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
然后,在 运行ning 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
添加的用例我认为这可以通过修改后的数据模型来简化
为了使示例更清楚,我更改了 类 的名称以使其更具描述性。
我们将使用 类:
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