在PostgreSQL

时间:2019-01-25 16:55:00

标签: ruby-on-rails postgresql

我有一个很大的数据集,我想对记录重叠时间的计数求和。例如,给定数据

[
  {"id": 1, "name": 'A', "start": '2018-12-10 00:00:00', "end": '2018-12-20 00:00:00', count: 34},
  {"id": 2, "name": 'B', "start": '2018-12-16 00:00:00', "end": '2018-12-27 00:00:00', count: 19},
  {"id": 3, "name": 'C', "start": '2018-12-16 00:00:00', "end": '2018-12-20 00:00:00', count: 56},
  {"id": 4, "name": 'D', "start": '2018-12-25 00:00:00', "end": '2018-12-30 00:00:00', count: 43}
]

enter image description here

您可以看到活动有两个重叠的时期。我想根据重叠中涉及的活动返回这些“重叠”的总数。因此,上面的输出将类似于:

[
  {start:'2018-12-16', end: '2018-12-20', overlap_ids:[1,2,3], total_count: 109},
  {start:'2018-12-25', end: '2018-12-27', overlap_ids:[2,4], total_count: 62},
]

问题是,如何通过postgres查询生成此信息?正在查看generate_series,然后计算出每个时间间隔内有哪些活动,但这并不完全正确,因为数据是连续的-我真的需要确定确切的重叠时间,然后对重叠的活动进行求和。

EDIT添加了另一个示例。正如@SRack所指出的,由于A,B,C重叠,这意味着B,CA,A,B和A,C也重叠。没关系,因为我要查找的输出是日期范围的数组,它们包含重叠活动,而不是重叠的所有唯一组合。另请注意,日期是时间戳记,因此将具有毫秒级的精度,并且不一定都在00:00:00。 如果有帮助,则总数上可能会存在WHERE条件。例如,只想查看总数> 100的结果 enter image description here

2 个答案:

答案 0 :(得分:1)

demo:db<>fiddle(使用带有重叠的A-B部分的旧数据集)

免责声明::此功能适用于日期间隔,而不适用于时间戳记。 ts的要求稍后出现。

SELECT
    s.acts,
    s.sum,
    MIN(a.start) as start,
    MAX(a.end) as end
FROM (
    SELECT DISTINCT ON (acts)
        array_agg(name) as acts,
        SUM(count)
    FROM
        activities, generate_series(start, "end", interval '1 day') gs
    GROUP BY gs
    HAVING cardinality(array_agg(name)) > 1
) s
JOIN activities a
ON a.name = ANY(s.acts)
GROUP BY s.acts, s.sum
  1. generate_series生成开始和结束之间的所有日期。因此,活动存在的每个日期都会与特定的count
  2. 对应一行
  3. 将所有日期分组,汇总所有现有活动及其计数总和
  4. HAVING过滤掉只有一项活动的日期
  5. 因为在同一天有不同的活动,所以我们只需要一名代表:用DISTINCT ON过滤所有重复项
  6. 将此结果与原始表结合起来以获取开始和结束。 (请注意,“ end”是Postgres中的保留字,您最好找到另一个列名!)。以前丢失它们会更舒服,但是可以在子查询中获取这些数据。
  7. 将此联接分组以获取每个间隔的最早和最新日期。

这是时间戳的版本:

demo:db<>fiddle

WITH timeslots AS (
    SELECT * FROM (
        SELECT
            tsrange(timepoint, lead(timepoint) OVER (ORDER BY timepoint)),
            lead(timepoint) OVER (ORDER BY timepoint)     -- 2
        FROM (
            SELECT 
                unnest(ARRAY[start, "end"]) as timepoint  -- 1 
            FROM
                activities
            ORDER BY timepoint
        ) s
    )s  WHERE lead IS NOT NULL                            -- 3
)
SELECT 
    GREATEST(MAX(start), lower(tsrange)),                 -- 6
    LEAST(MIN("end"), upper(tsrange)),
    array_agg(name),                                      -- 5
    sum(count)
FROM 
    timeslots t
JOIN activities a
ON t.tsrange && tsrange(a.start, a.end)                   -- 4
GROUP BY tsrange
HAVING cardinality(array_agg(name)) > 1

主要思想是确定可能的时隙。因此,我将每个已知的时间(开始和结束)都放入一个已排序的列表中。因此,我可以进行第一个已知的拖曳时间(从起点A开始的17:00和从起点B开始的18:00),并检查其中的间隔。然后我检查第二和第三,然后第三和第四,依此类推。

在第一个时隙中,仅A适用。在18-19的第二个中,B也适合。在下一个19-20插槽中,也C,从20到20:30 A不再适合,仅B和C。下一个是20:30-22,其中仅B适合,最后将22-23 D添加到B,最后但并非唯一的D都适合23-23:30。

因此,我选择了这个时间清单,并将其加入到间隔相交的活动表中。之后,仅按时间分组并汇总计数即可。

  1. 这会将一行中的所有ts放入一个数组,该数组的元素使用unnest扩展为每个元素的一行。所以我把所有时间都放在一列中,可以简单地排序
  2. 使用引号window function可以将下一行的值带入当前行。因此,我可以使用tsrange
  3. 从这两个值中创建一个时间戳范围
  4. 此过滤器是必需的,因为最后一行没有“下一个值”。这将创建一个NULL值,该值由tsrange解释为无穷大。因此,这将产生令人难以置信的错误时隙。因此,我们需要将此行过滤掉。
  5. 将时隙加入到原始表中。 &&运算符检查两个范围类型是否重叠。
  6. 按单个时隙分组,汇总名称和计数。使用HAVING子句
  7. 仅过滤一项活动的时隙
  8. 获取正确的起点和终点有些棘手。因此,起点是活动开始的最大值或时隙的开始(可以使用lower获取)。例如。以20-20:30时隙为例:它开始20h,但B和C都没有起点。结束时间类似。

答案 1 :(得分:0)

因为它被标记为Ruby on Rails,所以我也为此提供了一个Rails解决方案。我更新了数据,使它们不会全部重叠,并使用以下方法:

data = [
  {"id": 1, "name": 'A', "start": '2017-12-10 00:00:00', "end": '2017-12-20 00:00:00', count: 34},
  {"id": 2, "name": 'B', "start": '2018-12-16 00:00:00', "end": '2018-12-21 00:00:00', count: 19},
  {"id": 3, "name": 'C', "start": '2018-12-20 00:00:00', "end": '2018-12-29 00:00:00', count: 56},
  {"id": 4, "name": 'D', "start": '2018-12-21 00:00:00', "end": '2018-12-30 00:00:00', count: 43}
]

(2..data.length).each_with_object({}) do |n, hash|
  data.combination(n).each do |items|
    combination = items.dup
    first_item = combination.shift
    first_item_range = (Date.parse(first_item[:start])..Date.parse(first_item[:end]))

    if combination.all? { |i| (Date.parse(i[:start])..Date.parse(i[:end])).overlaps?(first_item_range) }
      hash[items.map { |i| i[:name] }.sort] = items.sum { |i| i[:count] }
    end
  end
end

我已经更新了数据,所以它们不会全部重叠,从而产生以下结果:

# => {["B", "C"]=>75, ["B", "D"]=>62, ["C", "D"]=>99, ["B", "C", "D"]=>118}

...因此,您可以看到项BCD重叠,总数为118。 (自然,这也意味着B, CB, DC, D重叠。)

这是逐步执行的操作:

  • 获取长度为2到4(数据长度)的数据条目的每种组合
  • 重复这些内容,并将组合的第一个元素与其他元素进行比较
  • 如果所有这些重叠,请将其存储在哈希中

通过这种方式,我们获得了唯一的数据名称条目,并在它们旁边存储了一个计数。

希望这是有用的-乐意就可以改进此问题的任何方面收集反馈。让我知道你过得怎么样!