首页 > 解决方案 > 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 实例在更大的数据集上内存不足。

标签: ruby-on-railsrubyloopsquery-optimization

解决方案


绝对不是最优的。即使仅使用您提供的内容(真的很奇怪的“算法”,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], 等

对于更有意义的事情,你必须解释什么somethingupdate variables正在做什么。根据您要执行的操作,您的计算可以在单个查询中完成。

更新问题的更新

你基本上在这里遇到了一个不平凡的调度和搜索问题,老实说:要真正很好地解决这个问题,你需要学习很多关于调度和搜索算法的知识,这两者都超出了范围对于 StackOverflow 问题。

至少,我会建议两件事,它们仍然可以极大地改进这个蛮力解决方案:

模型“活动时间窗口”

连续递增start_date和周期性地“跳回”是非常尴尬的,并且表明它不是一个很好的问题模型。

相反,想想一个有开始和结束的“活动时间窗口”。你正在“及时向前滑动窗口”试图找到合适的地方。无论你是否找到一个插槽,你永远不会“从末端跳回来”,因为窗口只会向前移动,你可以time_window.beginning在需要的时候得到它。

不要重新获取数据

I/O(如 db 查询)比数据处理慢 1-10 个数量级。重新获取是一个巨大的时间浪费。

注意你start_date 永远不会倒退(我们现在使用时间窗口),你会看到你的第一个weather_data.where("timestamp >= ?", start_date)调用将成为所有后续调用的超集。如果您要在第一次查询所有数据,请不要稍后再重新获取。


推荐阅读