Я пытаюсь объединить несколько диапазонов дат (моя загрузка составляет около 500, в большинстве случаев 10), которые могут перекрывать или не перекрывать максимально возможные диапазоны дат. Например:
Данные:
CREATE TABLE test (
id SERIAL PRIMARY KEY NOT NULL,
range DATERANGE
);
INSERT INTO test (range) VALUES
(DATERANGE('2015-01-01', '2015-01-05')),
(DATERANGE('2015-01-01', '2015-01-03')),
(DATERANGE('2015-01-03', '2015-01-06')),
(DATERANGE('2015-01-07', '2015-01-09')),
(DATERANGE('2015-01-08', '2015-01-09')),
(DATERANGE('2015-01-12', NULL)),
(DATERANGE('2015-01-10', '2015-01-12')),
(DATERANGE('2015-01-10', '2015-01-12'));
Таблица выглядит так:
id | range
----+-------------------------
1 | [2015-01-01,2015-01-05)
2 | [2015-01-01,2015-01-03)
3 | [2015-01-03,2015-01-06)
4 | [2015-01-07,2015-01-09)
5 | [2015-01-08,2015-01-09)
6 | [2015-01-12,)
7 | [2015-01-10,2015-01-12)
8 | [2015-01-10,2015-01-12)
(8 rows)
Желаемые результаты:
combined
--------------------------
[2015-01-01, 2015-01-06)
[2015-01-07, 2015-01-09)
[2015-01-10, )
Визуальное представление:
1 | =====
2 | ===
3 | ===
4 | ==
5 | =
6 | =============>
7 | ==
8 | ==
--+---------------------------
| ====== == ===============>
postgresql
aggregate
range-types
Вилье Штраус
источник
источник
Ответы:
Предположения / Разъяснения
Не нужно различать
infinity
и открывать верхнюю границу (upper(range) IS NULL
). (Вы можете сделать это в любом случае, но так проще.)infinity
в диапазоне типов PostgreSQLПоскольку
date
это дискретный тип, все диапазоны имеют[)
границы по умолчанию . По документации:Для других типов (например
tsrange
,!) Я бы применил то же самое, если это возможно:Решение с использованием чистого SQL
С CTE для ясности:
Или то же самое с подзапросами, быстрее, но не так легко читается:
Или с одним меньшим уровнем подзапроса, но с измененным порядком сортировки:
ORDER BY range DESC NULLS LAST
(сNULLS LAST
), чтобы получить полностью перевернутый порядок сортировки. Это должно быть дешевле (проще производить, точно соответствовать порядку сортировки предлагаемого индекса) и быть точным для угловых случаев сrank IS NULL
.объяснять
a
: При упорядочении поrange
, вычислите рабочий максимум верхней границы (enddate
) с помощью оконной функции.Замените NULL-границы (неограниченные) на +/-
infinity
просто для упрощения (без особых NULL-случаев).b
: В том же порядке сортировки, если предыдущийenddate
более ранний, чемstartdate
у нас, есть пробел и начинается новый диапазон (step
).Помните, верхняя граница всегда исключается.
c
: Сформировать группы (grp
) путем подсчета шагов с помощью другой оконной функции.Во внешней
SELECT
сборке колеблется от нижней до верхней границы в каждой группе. Вуаля.Тесно связанный ответ на SO с дополнительным объяснением:
Процедурное решение с plpgsql
Работает для любого имени таблицы / столбца, но только для типа
daterange
.Процедурные решения с циклами обычно медленнее, но в этом особом случае я ожидаю, что функция будет значительно быстрее, поскольку требуется только одно последовательное сканирование :
Вызов:
Логика аналогична решениям SQL, но мы можем обойтись за один проход.
SQL Fiddle.
Связанный:
Обычное упражнение для обработки пользовательского ввода в динамическом SQL:
Показатель
Для каждого из этих решений простой (по умолчанию) индекс btree
range
будет способствовать производительности в больших таблицах:Индекс btree имеет ограниченное использование для типов диапазонов , но мы можем получать предварительно отсортированные данные и, возможно, даже сканирование только по индексу.
источник
EXPLAIN ( ANALYZE, TIMING OFF)
сравнивать с лучшими из пяти.max(COALESCE(upper(range), 'infinity')) OVER (ORDER BY range) AS enddate
? Разве это не может быть простоCOALESCE(upper(range), 'infinity') as enddate
? AFAIKmax() + over (order by range)
вернется толькоupper(range)
сюда.Я придумал это:
Все еще нужно немного отточить, но идея заключается в следующем:
+
сбой union ( ), вернуть уже построенный диапазон и повторно инициализироватьисточник
generate_series()
на каждом ряду, особенно если могут быть открытые диапазоны ...Несколько лет назад я тестировал различные решения (среди прочих, похожие на @ErwinBrandstetter) для объединения перекрывающихся периодов в системе Teradata, и я нашел следующее наиболее эффективное (с использованием аналитических функций, в более новой версии Teradata есть встроенные функции для эта задача).
maxEnddate
maxEnddate
используяLEAD
и вы почти закончили. Только для последней строкиLEAD
возвращает aNULL
, для решения этой задачи вычислите максимальную дату окончания всех строк раздела на шаге 2 иCOALESCE
его.Почему это было быстрее? В зависимости от фактических данных шаг № 2 может значительно сократить количество строк, поэтому следующий шаг должен работать только с небольшим подмножеством, кроме того, он удаляет агрегацию.
играть на скрипке
Поскольку это было быстрее всего на Teradata, я не знаю, то же самое ли это для PostgreSQL, было бы неплохо получить реальные цифры производительности.
источник
Ради интереса я дал ему шанс. Я обнаружил, что это самый быстрый и чистый способ сделать это. Сначала мы определяем функцию, которая объединяется, если есть перекрытие или если два входа смежны, если нет перекрытия или смежности, мы просто возвращаем первый диапазон дат. Подсказка
+
- это объединение диапазонов в контексте диапазонов.Тогда мы используем это так,
источник
('2015-01-01', '2015-01-03'), ('2015-01-03', '2015-01-05'), ('2015-01-05', '2015-01-06')
.