【问题标题】:Recognize if not enough resources during timeframe识别时间范围内是否没有足够的资源
【发布时间】:2017-08-23 22:58:30
【问题描述】:

我想知道如何解决以下问题。

假设我有一组具有给定时间范围(8:00-12:00),我可以为它分配资源(人员)。每个资源都可以有一个自定义的时间范围(如 9-10、9-12、8-12 等),并且可以多次分配。

表格

身份证, 标题, 开始时间, 时间结束, REQUIRED_PEOPLE:整数

人员分配

身份证, 用户身份, GROUP_ID, 开始时间, END_TIME

现在我有一个规则,即在小组时间范围内的任何给定时间,必须分配 4 个人。否则我想收到警告。

我在这里使用 ruby​​ 和 sql (Postgres)。

有没有一种优雅的方法,无需遍历整个时间范围并检查 count(assignments) > REQUIRED_PEOPLE

【问题讨论】:

  • 到目前为止你做了什么?展示您的解决方案,以便我们判断它是否已经优雅
  • 到目前为止,我有一个“便宜”的近似值。 (REQUIRED_PEOPLE * GROUP_DURATION) - SUM(ASSIGNMENT_DURATIONS_FOR_GROUP) 但是当我有十个人同时分配一小时时,这不会解决问题。

标签: sql ruby-on-rails algorithm postgresql search


【解决方案1】:

你也可以只用 SQL 来解决这个问题(如果你对这样的答案感兴趣的话)。

Range types 提供了很好的 functions and operators 进行计算。

当有子范围时,这些解决方案将为您提供行,其中有给定组中的一些失踪人员(它会给您确切的子范围以及所需人数中缺少多少人)。

简单的方法:

您想尝试类似的方法。您需要选择count() 所基于的某个区间(我选择了5 minutes):

select     g.id group_id, i start_time, i + interval '5 minutes' end_time, g.required_people - count(a.id)
from       groups g
cross join generate_series(g.start_time, g.end_time, interval '5 minutes') i
left join  people_assignments a on a.group_id = g.id
where      tsrange(a.start_time, a.end_time) && tsrange(i, i + interval '5 minutes')
group by   g.id, i
having     g.required_people - count(a.id) > 0
order by   g.id, i

但请注意,当缺少的子范围小于 5 minutes 时,您将无法检测到它们。前任user1 分配给11:00-11:56,而user2 分配给11:59-13:00,他们似乎在11:00-13:00 的组中(因此11:56-11:59 的缺失子范围将不被注意)。

注意:间隔越短(你选择的)结果就越精确(而且慢!)。

http://rextester.com/GRC64969

艰难的道路

您可以使用custom aggregatesrecursive CTEs 即时累积结果

with recursive r as (
    -- start with "required_people" as "missing_required_people" in the whole range
    select 0 iteration,
           id group_id,
           array[]::int[] used_assignment_ids,
           -- build a json map, where keys are the time ranges
           -- and values are the number of missing people for that range
           jsonb_build_object(tsrange(start_time, end_time), required_people) required_people_per_time_range
    from   groups
    where  required_people > 0
    and    id = 1 -- query parameter
  union all
    select r.iteration + 1,
           r.group_id,
           r.used_assignment_ids || a.assignment_id,
           d.required_people_per_time_range
    from   r
    -- join a single assignment to the previous iteration, where
    -- the assigment's time range overlaps with (at least one) time range,
    -- where there is still missing people. when there are no such time range is
    -- found in assignments, the "recursion" (which is really just a loop) stops
    cross join lateral (
      select     a.id assignment_id, tsrange(start_time, end_time) time_range
      from       people_assignments a
      cross join (select key::tsrange time_range from jsonb_each(r.required_people_per_time_range)) j
      where      a.group_id = r.group_id
      and        a.id <> ALL (r.used_assignment_ids)
      and        tsrange(start_time, end_time) && j.time_range
      limit      1
    ) a
    -- "partition" && accumulate all remaining time ranges with
    -- the one found in the previous step
    cross join lateral (
      -- accumulate "partition" results
      select jsonb_object_agg(u.time_range, u.required_people) required_people_per_time_range
      from   (select key::tsrange time_range, value::int required_people
              from   jsonb_each_text(r.required_people_per_time_range)) j
      cross join lateral (
        select u time_range, j.required_people - case when u && a.time_range then 1 else 0 end required_people
        -- "partition" the found time range with all existing ones, one-by-one
        from   unnest(case
                 when j.time_range @> a.time_range
                 then array[tsrange(lower(j.time_range), lower(a.time_range)), a.time_range, tsrange(upper(a.time_range), upper(j.time_range))]
                 when j.time_range && a.time_range
                 then array[j.time_range * a.time_range, j.time_range - a.time_range]
                 else array[j.time_range]
               end) u
        where not isempty(u)
      ) u
    ) d
),
-- select only the last iteration
l as (
  select   group_id, required_people_per_time_range
  from     r
  order by iteration desc
  limit    1
)
-- unwind the accumulated json map
select     l.group_id, lower(time_range) start_time, upper(time_range) end_time, missing_required_people
from       l
cross join lateral (
  select key::tsrange time_range, value::int missing_required_people
  from   jsonb_each_text(l.required_people_per_time_range)
) j
-- select only where there is still some missing people
-- this is optional, if you omit it you'll also see row(s) for sub-ranges where
-- there is enough people in the group (these rows will have zero,
-- or negative amount of "missing_required_people")
where      j.missing_required_people > 0

http://rextester.com/GHPD52861

【讨论】:

  • 感谢您的回答,我认为我会采用简单的方法。我只允许在 15 分钟内完成作业,应该这样做
  • 嗯,我们目前在某些机器上使用 Postgres 9.1,但该解决方案出现错误。我将其追踪到一个基本查询,如SELECT g.id FROM groups g CROSS JOIN generate_series(g.start_time, g.end_time, interval '5 minutes') i,它适用于 9.5 但不适用于 9.1。它抱怨无效的表引用 g。一般来说,我无法在该函数中使用任何表格。有什么想法吗?
  • @Skully 称为 LATERAL 加入,从 9.3+ 开始支持。您可以使用correlated subqueries 在早期版本上模拟它。虽然 9.1 已经很老了,并且不再支持(最早但仍受支持的版本目前是 9.2 分支)。我强烈推荐更新。
【解决方案2】:

在任何情况下,您都需要在 DB 中查询分配数。没有其他方法可以计算一个组分配给人员的次数。

可能有一些方法可以找到分配的数量,但最终您必须向 DB 发起查询。

@group = Group.find(id)
if @group.people_assignments.count >= REQUIRED_PEOPLE
  pus 'warning'
end

您可以在组中添加额外的列,其中包含该组分配给人员的次数信息。这样就减少了对服务器的一次查询。

@group = Group.find(id)
if @group.count_people_assigned >= REQUIRED_PEOPLE
  puts 'warning'
end

在第二种情况下,count_people_assigned 是列,因此不会执行额外的查询,而在第一种情况下,people_assignments 是关联,因此会触发一个额外的查询。

但在第二种情况下,每次将组分配给人员时,您都会更新组。最终额外的查询。您选择要减少查询的位置。

我的意见是第二种情况,它会比第一种情况更罕见。

【讨论】:

  • 谢谢,但没那么简单。我想确保在组的时间范围内的每一秒内总是分配有 REQUIRED_PEOPLE。由于可以通过自定义时间范围分配人员,因此仅计算他们不会完成
猜你喜欢
  • 2019-06-21
  • 2018-01-27
  • 2019-03-12
  • 1970-01-01
  • 2021-05-15
  • 1970-01-01
  • 2020-05-29
  • 2011-12-20
  • 2015-10-28
相关资源
最近更新 更多