我有一个谜题,我希望有人可以帮我解决
我有一张公寓房源表。每个商家信息都有一个或多个“不可用”日期分别存储在另一个表格中,我们称之为“off_days”。例如从9月1日到4月4日不可用的列表将在'off_days'表中有4个条目,每天一个。
我正在寻找最有效的搜索方式(最好是在数据库级别),以便在两个日历日之间至少有N个连续可用天数('可用'是任何一天不在'off_days'表中特别列表)。例如“在9月份至少连续5天显示所有列表”
我一直在思考如何在现实世界中解决这个问题(通过查看标有X的日历并扫描空闲块)并开始考虑使用二进制来表示可用/不可用的天数。即对于给定的一周,0111001(= 57)会告诉我该周连续可用的天数最多为三天。
这个question似乎是一个良好的开端,一旦我有给定日期范围的二进制数,但现在我仍然坚持如何在给定日期范围内动态计算该数字,再次,在数据库上水平....任何想法?或者对这种方法或其他方法的看法?
答案 0 :(得分:2)
在休息日,公寓可供出租。这意味着您想知道每个序列的差距有多大,lag()
函数可以为您提供以下信息:
select od.*,
lag(unavailable) over (partition by apartmentid order by unavailable) as prev_una
from offdays od;
实际天数是不可用和上一个减1之间的差异。现在,假设两个日历日为v_StartDate
和v_EndDate
。现在你基本上可以得到你想要的东西:
select od.*,
((case when unavailable is NULL or unavailable > v_EndDate
then v_EndDate + 1 else unavailable
end) -
(case when prev_una is null or prev_una < v_StartDate
then v_StartDate - 1 else prev_una
end) - 1
) as days_available
from (select od.*, lag(unavailable) over (partition by apartmentid order by unavailable) as prev_una
from offdays od
) od
order by days_available desc;
case
逻辑基本上是在句号之前和之后放置停止日期。
这并不完全,因为它有边界问题:公寓不在offdays
时出现问题,而在不可用时段超出范围时出现问题。我们使用union all
和一些过滤来解决此问题:
select od.*,
((case when unavailable is NULL or unavailable > v_EndDate
then v_EndDate + 1 else unavailable
end) -
(case when prev_una is null or prev_una < v_StartDate
then v_StartDate - 1 else prev_una
end) - 1
) as days_available
from (select od.apartmentId, unavailable,
lag(unavailable) over (partition by apartmentid order by unavailable) as prev_una
from offdays od
where od.unavailable between v_StartDate and v_EndDate
union all
select apartmentid, NULL, NULL
from apartments a
where not exists (select 1
from offdays od
where od.apartmentid = a.apartmentid and
od.unavailable between v_StartDate and v_EndDate
)
) od
order by days_available desc;
答案 1 :(得分:0)
即使我对Rails一无所知,我还是建议采用一种方法。如果我的建议毫无意义,请告诉我,我将删除我的答案并随意删除。
假设您仅询问数据库以创建一个数组,该数组指示公寓在连续日期范围内的每个日期是否可用。例如,假设它看起来像这样:
A = true
U = nil
avail = [A,A,A,U,U,A,A,U,U,A,A,A,A,A,U,A]
对于给定数量的连续日期n
,可以通过以下方法给出开始运行至少n
天的日期偏移。
<强>代码强>
def runs(avail, n)
avail.each_with_index.each_cons(n).map do |run|
av, off = run.transpose
(av == av.compact) ? off.first : nil
end.compact
end
<强>实施例强>
runs(avail,1) #=> [0, 1, 2, 5, 6, 9, 10, 11, 12, 13, 15]
runs(avail,2) #=> [0, 1, 5, 9, 10, 11, 12]
runs(avail,3) #=> [0, 9, 10, 11]
runs(avail,4) #=> [9, 10]
runs(avail,5) #=> [9]
runs(avail,6) #=> []
<强>解释强>
考虑上面的n=3
案例。
n = 3
enum0 = avail.each_with_index
#=> #<Enumerator: [true, true, true, nil, nil, true, true, nil, nil,
# true, true, true, true, true, nil, true]:each_with_index>
enum0.to_a
#=> [[true, 0], [true, 1], [true, 2], [nil, 3], [nil, 4], [true, 5],
# [true, 6], [nil, 7], [nil, 8], [true, 9], [true, 10], [true, 11],
# [true, 12], [true, 13], [nil, 14], [true, 15]]
enum1 = enum0.each_cons(n)
#=> #<Enumerator: #<Enumerator: [true, true, true, nil,...
# ..., true]:each_with_index>:each_cons(40)>
enum1.to_a
#=> [[[true, 0], [true, 1], [true, 2]],
# [[true, 1], [true, 2], [nil, 3]],
# ...
# [[true, 13], [nil, 14], [true, 15]]]
enum2 = enum1.map
#=> #<Enumerator: #<Enumerator: #<Enumerator: [true, true, true, nil,...
# ...true]:each_with_index>:each_cons(3)>:map>
enum2.to_a
#=> [[[true, 0], [true, 1], [true, 2]],
# [[true, 1], [true, 2], [nil, 3]],
# ...
# [[true, 13], [nil, 14], [true, 15]]]
a = enum2.each do |run|
av, off = run.transpose
(av == av.compact) ? off.first : nil
end
#=> [0, nil, nil, nil, nil, nil, nil, nil, nil, 9, 10, 11, nil, nil]
a.compact
#=> [0, 9, 10, 11]
考虑上面的数组a
的计算。 enum2
传递给块的each
的第一个元素,并分配给块变量run
:
run => [[true, 0], [true, 1], [true, 2]]
然后
av, off = run.transpose #=> [[true, true, true], [0, 1, 2]]
av #=> [true, true, true]
off #=> [0, 1, 2]
([true, true, true] == [true, true, true].compact) ? 0 : nil
#=> ([true, true, true] == [true, true, true]) ? 0 : nil
#=> 0
因此enum2
的第一个值会映射到0
,这意味着从偏移量3
开始至少有0
天的运行。
接下来,[[true, 1], [true, 2], [nil, 3]]
被传递到块中并分配给变量run
。然后:
av, off = run.transpose #=> [[true, true, nil], [1, 2, 3]]
av #=> [true, true, nil]
off #=> [1, 2, 3]
([true, true, nil] == [true, true, nil].compact) ? 1 : nil
# ([true, true, nil] == [true, true]) ? 1 : nil
#=> nil
因此enum2
的第二个值会映射到nil
,这意味着从偏移量3
开始,至少1
天没有。等等...
备注强>
avail
中指示公寓的值在给定的日期(上面的常数A
)可用,可以是任何“真实”值(即false
以外的任何值或nil
);但是,不可用日期(由上面U
表示)必须由nil
表示,因为我使用Array#compact将其从数组中删除。 enum1
和enum2
视为“复合”枚举数可能会有所帮助。enum0.to_a
,enum1.to_a
和enum2.to_a
用于显示每个枚举器将传递到其块中的内容(如果有的话)。 (enum2
当然有一个区块。)答案 2 :(得分:0)
使用generate_series()
和两个嵌套的EXISTS
表达式,您可以将这个英语句子直接翻译成SQL:
Find apartments where at least one day exists in a given time range (September) where no "off_day" exists in a 5-day range starting that day.
SELECT *
FROM apt a
WHERE EXISTS (
SELECT 1
FROM generate_series('2014-09-01'::date
, '2014-10-01'::date - 5
, interval '1 day') r(day)
WHERE NOT EXISTS (
SELECT 1
FROM offday o
WHERE o.apt_id = a.apt_id
AND o.day BETWEEN r.day::date AND r.day::date + 4
)
)
ORDER BY a.apt_id; -- optional
您可以应用类似的查询来获取实际免费广告位列表(开始日期):
SELECT *
FROM apt a
-- FROM (SELECT * FROM apt a WHERE apt_id = 1) a -- for just agiven apt
CROSS JOIN generate_series('2014-09-01'::date
, '2014-10-01'::date - 5
, interval '1 day') r(day)
WHERE NOT EXISTS (
SELECT 1
FROM offday o
WHERE o.apt_id = a.apt_id
AND o.day BETWEEN r.day::date AND r.day::date + 4
)
ORDER BY a.apt_id, r.day;
仅适用于给定的。:
中的时段SELECT *
FROM (SELECT * FROM apt a WHERE apt_id = 1) a
...
如果 “off-days”通常包含连续多天,则基于date ranges的替代表格布局而不是单日是一个(更有效)的替代方案。
Range operators可以在范围列上使用GiST索引。我的第一个答案草案建立在ad-hoc范围(see answer history)上,但在使用“每天一行”设计时,更新的解决方案更简单,更快捷。具有适应性查询的替代布局:
相关答案与基本信息的类似实施: