ruby-on-rails - Ruby:通过时间戳循环并跳回循环 - 优化
问题描述
我们在 Ruby on Rails / Postgres 中有一个数据库表,其中包含多达 100.000 个跨年的天气数据点,按小时计算:
01/01/1999 00:00
01/01/1999 01:00
...
01/01/2000 00:00
日期保存在一个datetime
名为 的变量中timestamp
。
我们正在迭代weather_data
,有时我们需要跳回 1-3 小时,以再次检查不同的条件。
然后我们有多个活动,每个活动持续 1-6 个小时,这取决于天气是否足够好,或者他们是否需要等到天气好转。
用户可以选择一年中的哪一天开始检查,但它将从那一天开始检查数据库中的每一年。
如果用户选择“1997 年 4 月 3 日”,它应该从该日期开始运行所有活动,并查看所有活动需要多长时间。
然后它应该对“1998 年 4 月 3 日”和 1999 年以及所有可用年份重复该过程weather_data
有些活动可能需要 2 小时,但他们需要提前 4 小时了解天气,即使下一个活动可以在 2 小时后开始。所以有一点重叠。我希望用变量来解决这个问题,但无法弄清楚,因此我想到了在循环中来回“跳跃”。
简化示例:
# Collect all the years
the_years = weather_data.map { |y| y.timestamp.year }.uniq
the_years.each do |year|
start_date = DateTime.new(year, user_input.month, user_input.day)
# We could have ~100 activities
activities.each do |activity|
consecutive_good_weather_hours = 0
weather_data.where("timestamp >= ?", start_date).each do |point|
start_date += 1.hour
# checking if point.wind_speed > activity.wind_speed etc.
if weather_is_good
# ...
consecutive_good_weather_hours += 1
# if this activity needs 3 hours of good weather, and we have 2/3
# we go to the next data point, to check the next hour.
# go to next activity if all criteria is met
if activity_finished
# if this activity was 3 hours long, but we were checking 2
# hours extra into the future, we need to 'jump back' 2 hours
# where the next activity should start, a bit of overlap
start_date -= 2.hours
break
end
else
# bad weather, reset counter, and go to next weather hour
# try again to find x many hours of consecutive good weather
consecutive_good_weather_hours = 0
end
end
end
end
这有多优化?
看起来我们正在执行一个新的 SQL 查询 300 次,加载约 100k 的数据集(虽然每次都会缩小一点)。
我们可以在循环中向后“跳” 3 步,而不是一直调用.where
吗?如果是,如何?
编辑 1
我们将其替换weather_data.where("timestamp >= ?", start_date).each do |point|
为以下内容:
while true
point = weather_data.find_by_timestamp(year_start_date)
我们还尝试weather_data
使用(在所有循环之外)复制到数组中.to_a
,然后执行以下操作:
while true
point = data_array.find { |i| i.timestamp == year_start_date }
但事实证明速度较慢,请参阅基准。
20k 数据点和 4 项活动的基准测试:
| Option | points | ms | Allocations |
|-------------------|--------|------|-------------|
| where | 20k | 3028 | 5931134 |
| find_by_timestamp | 20k | 1101 | 725407 |
| data_array.find | 20k | 1304 | 1393532 |
我认为它find_by_timestamp
会比 the 慢,array.find
因为它会SELECT
在每一点上做一个,但它看起来是 3 中最快的。
我们正在使用 Heroku,我们的 1GB 实例在更大的数据集上内存不足。
解决方案
绝对不是最优的。即使仅使用您提供的内容(真的很奇怪的“算法”,tbh),很明显您不断地一遍又一遍地重新获取相同的数据行。
使用简化数据集的演示:
dataset = (0..9).to_a
start_index = 8
5.times do
queried = dataset.select { |d| d >= start_index } # same as your WHERE clause, in principle
p queried
queried.each do |idx|
if idx.even?
start_index -= 3
break
else
start_index += 1
end
end
end
将打印:
[8, 9]
[5, 6, 7, 8, 9]
[3, 4, 5, 6, 7, 8, 9]
[1, 2, 3, 4, 5, 6, 7, 8, 9]
看看它是如何不断地一遍又一遍地重新获取相同的值?[8, 9]
, [..., 8, 9]
, 等
对于更有意义的事情,你必须解释什么something
和update variables
正在做什么。根据您要执行的操作,您的计算可以在单个查询中完成。
更新问题的更新
你基本上在这里遇到了一个不平凡的调度和搜索问题,老实说:要真正很好地解决这个问题,你需要学习很多关于调度和搜索算法的知识,这两者都超出了范围对于 StackOverflow 问题。
至少,我会建议两件事,它们仍然可以极大地改进这个蛮力解决方案:
模型“活动时间窗口”
连续递增start_date
和周期性地“跳回”是非常尴尬的,并且表明它不是一个很好的问题模型。
相反,想想一个有开始和结束的“活动时间窗口”。你正在“及时向前滑动窗口”试图找到合适的地方。无论你是否找到一个插槽,你永远不会“从末端跳回来”,因为窗口只会向前移动,你可以time_window.beginning
在需要的时候得到它。
不要重新获取数据
I/O(如 db 查询)比数据处理慢 1-10 个数量级。重新获取是一个巨大的时间浪费。
注意你start_date
永远不会倒退(我们现在使用时间窗口),你会看到你的第一个weather_data.where("timestamp >= ?", start_date)
调用将成为所有后续调用的超集。如果您要在第一次查询所有数据,请不要稍后再重新获取。