【问题标题】:Ruby: Looping through timestamps and jumping back in the loop - optimizationRuby:通过时间戳循环并跳回循环 - 优化
【发布时间】:2020-10-17 05:25:21
【问题描述】:

我们在 Ruby on Rails / Postgres 中有一个数据库表,其中包含多达 100.000 个跨年的天气数据点,按小时计算:

01/01/1999 00:00
01/01/1999 01:00
...
01/01/2000 00:00

日期保存在名为timestampdatetime 变量中。

我们正在迭代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

这有多优化?

看起来我们正在执行 300 次新的 SQL 查询,加载约 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 会比array.find 慢,因为它会在每一点上执行SELECT,但它看起来是三个中最快的。

我们正在使用 Heroku,但我们的 1GB 实例在更大的数据集上内存不足。

【问题讨论】:

  • 哈希应该更快,但这仍然不是你真正的问题,这可以通过记忆来解决,你应该询问 start_date 和一小时之间的每一点,进行计算并保存它们进入哈希表,然后在询问查询之前检查哈希和结果,如果值不存在,请检查查询,观看此视频,它应该为您提供一个良好的起点youtube.com/watch?v=P8Xa2BitN3I,您目前正在做类似的事情O(n!) 时间,如果我做对了,它应该在 O(n) 时间内完成,或者根据你的基准 ~3s

标签: ruby-on-rails ruby loops query-optimization


【解决方案1】:

绝对不是最优的。即使仅使用您提供的内容(真的很奇怪的“算法”,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) 调用将成为所有后续调用的超集来电。如果您要在第一次查询所有数据,请不要稍后再重新获取。

【讨论】:

  • 谢谢,我添加了更多上下文,但尽量保持简单。如果示例太长,我认为希望提供帮助的人会更少!
  • 是的,我也是这么想的,看起来像是记忆问题
  • 已更新,希望对您有所帮助。
【解决方案2】:

如果只有 100k 条记录,则应该可以将所有内容加载到内存中。

如果您的数据集没有丢失记录(例如,从凌晨 1 点跳到凌晨 3 点而没有凌晨 2 点),您可以使用数组查找它们。

# convert to array for lookup
records = weather_data.where("timestamp >= ?", start_date).to_a

index = 0
while index < 300 
  record = records[index]
  if something
    # update variables
    index -= 3
    break
  else
    index += 1 # sometimes we jump further
  end
end

如果它太大而无法加载到内存中,您可以分组迭代。

【讨论】:

    猜你喜欢
    • 2012-03-02
    • 2015-04-10
    • 2016-12-07
    • 2021-01-21
    • 1970-01-01
    • 1970-01-01
    • 2017-07-20
    相关资源
    最近更新 更多