Найти и суммировать диапазоны дат с перекрывающимися записями в postgresql - PullRequest
0 голосов
/ 25 января 2019

У меня есть большой набор данных, где я хочу суммировать счет, где записи имеют перекрывающееся время. Например, с учетом данных

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

Вы можете видеть, что есть 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, а затем выяснял, какие действия попадают в каждый интервал, но это не совсем верно, поскольку данные непрерывны - мне действительно нужно определить точное время перекрытия, а затем подвести итог по перекрывающимся действиям.

РЕДАКТИРОВАТЬ Добавили еще один пример. Как указал @SRack, поскольку A, B, C перекрываются, это означает, что B, C A, B и A, C также перекрываются. Это не имеет значения, так как искомый вывод представляет собой массив диапазонов дат , которые содержат перекрывающиеся действия , а не все уникальные комбинации перекрытий. Также обратите внимание, что даты являются временными метками, поэтому они будут иметь точность в миллисекунды и не обязательно будут все в 00:00:00. Если это поможет, вероятно, будет общее условие ГДЕ в общем количестве. Например, хотите видеть только результаты, где общее количество> 100 enter image description here

Ответы [ 2 ]

0 голосов
/ 25 января 2019

Поскольку это помечено как 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}

... Таким образом, вы можете видеть, что элементы B, C и D перекрываются с общим числом 118. (Естественно, это также означает, что B, C, B, D и C, D перекрываются.)

Вот что это делает по шагам:

  • получает каждую комбинацию записей данных длиной от 2 до 4 (длина данных)
  • перебирает их и сравнивает первый элемент комбинации с остальными
  • если все они перекрываются, сохраните это в хэше

Таким образом, мы получаем уникальные записи имен данных, с подсчетом, сохраненным рядом с ними.

Надеюсь, это полезно - с удовольствием приму обратную связь в любом случае, в котором это можно улучшить. Дайте мне знать, как вы поживаете!

0 голосов
/ 25 января 2019

demo: db <> fiddle (использует старый набор данных с перекрывающейся AB-частью)

Отказ от ответственности: Это работает для дневных интервалов, а не для отметок времени,Требование к 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. Соедините этот результат с исходной таблицей, чтобы получить начало и конец.(обратите внимание, что «конец» - это зарезервированное слово в 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

Основная идея заключается в определении возможных временных интервалов.Поэтому я беру каждое известное время (как начало, так и конец) и помещаю их в отсортированный список.Таким образом, я могу взять первое известное время буксировки (17:00 от начала A и 18:00 от начала B) и проверить, какой интервал в нем.Затем я проверяю его на 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. , используя оконную функцию lead , позволяющую перенести значение следующего ряда в текущий.Поэтому я могу создать диапазон отметок времени из этих обоих значений с помощью tsrange
  3. . Этот фильтр необходим, поскольку в последней строке нет «следующего значения».Это создает значение NULL, которое интерпретируется tsrange как бесконечность.Так что это создаст невероятно неправильный временной интервал.Поэтому нам нужно отфильтровать эту строку.
  4. Соедините временные интервалы с исходной таблицей.Оператор && проверяет, перекрываются ли два типа диапазонов.
  5. Группировка по одиночным временным интервалам, объединение имен и количества.Отфильтруйте временные интервалы только с одним действием, используя предложение HAVING
  6. Немного сложно, чтобы получить правильные начальные и конечные точки.Таким образом, начальные точки являются либо максимумом начала активности, либо началом временного интервала (который можно получить, используя lower).Например, возьмите интервал 20-20: 30: он начинается через 20ч, но ни B, ни C не имеют там своей начальной точки.Похоже время окончания.
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...