【问题标题】:Find and sum date ranges with overlapping records in postgresql在 postgresql 中查找和汇总具有重叠记录的日期范围
【发布时间】:2019-01-25 16:54:59
【问题描述】:

我有一个大型数据集,我想在其中汇总记录具有重叠时间的计数。例如,给定数据

[
  {"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}
]

您可以看到有 2 个活动重叠的时期。我想根据重叠所涉及的活动返回这些“重叠”的总数。所以上面会输出类似:

[
  {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,C A,B 和 A,C 也重叠。这无关紧要,因为我要查找的输出是 日期范围 的数组,该数组包含重叠活动,而不是所有独特的重叠组合。另请注意,日期是时间戳,因此具有毫秒精度,不一定都在 00:00:00。 如果有帮助,总计数可能会有 WHERE 条件。例如,只想查看总计数 > 100 的结果

【问题讨论】:

  • 你有一些 JSON 数据还是这些表格行?
  • 开头的交叉口A B怎么办(没有C)
  • 以上只是 json 数据的一个缩减示例,是的,这些将是表格行(注意日期是完整的时间戳,而不仅仅是日期)
  • 好点@s-man - 将更新

标签: ruby-on-rails postgresql


【解决方案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. HAVING 过滤掉仅存在一项活动的日期
  4. 因为不同的日子有相同的活动,我们只需要一个代表:过滤所有重复的DISTINCT ON
  5. 将此结果与原始表相结合以获得开始和结束。 (请注意,“end”是 Postgres 中的保留字,您最好另找一个列名!)。以前丢失它们更舒服,但可以在子查询中获取这些数据。
  6. 将此联接分组以获得每个间隔的最早和最晚日期。

这是时间戳的一个版本:

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)并检查其中的间隔。然后我检查第 2 次和第 3 次,然后检查第 3 次和第 4 次,依此类推。

在第一个时间段中,只有 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. 此过滤器是必需的,因为最后一行没有“下一个值”。这将创建一个 NULL 值,tsrange 将其解释为无穷大。所以这会造成一个难以置信的错误时间段。所以我们需要过滤掉这一行。
  4. 根据原始表加入时隙。 &amp;&amp; 运算符检查两个范围类型是否重叠。
  5. 按单个时隙分组,聚合名称和计数。使用HAVING 子句过滤掉只有一个活动的时间段
  6. 获得正确的起点和终点有点棘手。所以起点要么是活动开始的最大值,要么是时间段的开始(可以使用lower 获取)。例如。以 20-20:30 时段为例:它从 20 小时开始,但 B 和 C 都没有起点。结束时间类似。

【讨论】:

  • 非常感谢您的快速响应!我查看了 generate_series 并通过设定的时间间隔进行操作并没有真正起作用,因为数据是连续的,实际日期具有毫秒精度。使用 1 分钟间隔可能是可以接受的,但理想情况下是寻找一种方法来识别准确重叠然后对计数求和。
  • 旁注 - 以前从未见过 dbfiddle,非常有用,谢谢!
  • 是的,这就是免责声明的原因。我想到了一些区间相​​交操作,但还没有找到方便的解决方案。如果您仍然需要它,我稍后会仔细查看。
  • 另请注意,上面 dbfiddle 中的输出不太正确,开始和结束应该是重叠的开始和结束,而不是活动开始/结束的最小/最大值。例如,重叠 A、B、C 的开始/结束日期为 2018-12-16 / 2018-12-20。间隔可以起作用,但前提是更小,例如 1 分钟,但这可能会影响更大数据集或日期范围的性能。希望这甚至是可能的......
  • 找到了解决办法! :)
【解决方案2】:

因为它被标记为 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(数据的长度)
  • 遍历这些元素并将组合的第一个元素与其他元素进行比较
  • 如果这些都重叠,则将其存储在哈希中

这样,我们可以获得唯一的数据名称条目,并在它们旁边存储一个计数。

希望这是有用的 - 很乐意就任何可以改进的地方提供反馈。告诉我你过得怎么样!

【讨论】:

  • 谢谢 - 有趣的解决方案并且绝对有用,会考虑但出于性能原因可能不会在 Rails 中执行此操作,实际数据集可能很大。请注意,重点较少关注哪些项目重叠,而更多关注哪些日期重叠以及相应的计数
  • 为@Dave 的反馈干杯 - 老实说,我很喜欢被困在其中,很高兴能把它放在一起。很高兴你得到了你想要的答案!
猜你喜欢
  • 2011-05-27
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-02-19
  • 1970-01-01
  • 1970-01-01
  • 2021-11-14
相关资源
最近更新 更多