Предположения / Разъяснения
Не нужно различать infinity
и открывать верхнюю границу ( upper(range) IS NULL
). (Вы можете сделать это в любом случае, но так проще.)
Поскольку date
это дискретный тип, все диапазоны имеют [)
границы по умолчанию .
По документации:
Встроенные типы дальности int4range
, int8range
и daterange
любое использование канонической форме , которая включает в себя нижнюю границу и не включает верхнюю границу; то есть [)
.
Для других типов (например tsrange
,!) Я бы применил то же самое, если это возможно:
Решение с использованием чистого SQL
С CTE для ясности:
WITH a AS (
SELECT range
, COALESCE(lower(range),'-infinity') AS startdate
, max(COALESCE(upper(range), 'infinity')) OVER (ORDER BY range) AS enddate
FROM test
)
, b AS (
SELECT *, lag(enddate) OVER (ORDER BY range) < startdate OR NULL AS step
FROM a
)
, c AS (
SELECT *, count(step) OVER (ORDER BY range) AS grp
FROM b
)
SELECT daterange(min(startdate), max(enddate)) AS range
FROM c
GROUP BY grp
ORDER BY 1;
Или то же самое с подзапросами, быстрее, но не так легко читается:
SELECT daterange(min(startdate), max(enddate)) AS range
FROM (
SELECT *, count(step) OVER (ORDER BY range) AS grp
FROM (
SELECT *, lag(enddate) OVER (ORDER BY range) < startdate OR NULL AS step
FROM (
SELECT range
, COALESCE(lower(range),'-infinity') AS startdate
, max(COALESCE(upper(range), 'infinity')) OVER (ORDER BY range) AS enddate
FROM test
) a
) b
) c
GROUP BY grp
ORDER BY 1;
Или с одним меньшим уровнем подзапроса, но с измененным порядком сортировки:
SELECT daterange(min(COALESCE(lower(range), '-infinity')), max(enddate)) AS range
FROM (
SELECT *, count(nextstart > enddate OR NULL) OVER (ORDER BY range DESC NULLS LAST) AS grp
FROM (
SELECT range
, max(COALESCE(upper(range), 'infinity')) OVER (ORDER BY range) AS enddate
, lead(lower(range)) OVER (ORDER BY range) As nextstart
FROM test
) a
) b
GROUP BY grp
ORDER BY 1;
- Сортируйте окно на втором шаге с помощью
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
.
Процедурные решения с циклами обычно медленнее, но в этом особом случае я ожидаю, что функция будет значительно быстрее, поскольку требуется только одно последовательное сканирование :
CREATE OR REPLACE FUNCTION f_range_agg(_tbl text, _col text)
RETURNS SETOF daterange AS
$func$
DECLARE
_lower date;
_upper date;
_enddate date;
_startdate date;
BEGIN
FOR _lower, _upper IN EXECUTE
format($$SELECT COALESCE(lower(t.%2$I),'-infinity') -- replace NULL with ...
, COALESCE(upper(t.%2$I), 'infinity') -- ... +/- infinity
FROM %1$I t
ORDER BY t.%2$I$$
, _tbl, _col)
LOOP
IF _lower > _enddate THEN -- return previous range
RETURN NEXT daterange(_startdate, _enddate);
SELECT _lower, _upper INTO _startdate, _enddate;
ELSIF _upper > _enddate THEN -- expand range
_enddate := _upper;
-- do nothing if _upper <= _enddate (range already included) ...
ELSIF _enddate IS NULL THEN -- init 1st round
SELECT _lower, _upper INTO _startdate, _enddate;
END IF;
END LOOP;
IF FOUND THEN -- return last row
RETURN NEXT daterange(_startdate, _enddate);
END IF;
END
$func$ LANGUAGE plpgsql;
Вызов:
SELECT * FROM f_range_agg('test', 'range'); -- table and column name
Логика аналогична решениям SQL, но мы можем обойтись за один проход.
SQL Fiddle.
Связанный:
Обычное упражнение для обработки пользовательского ввода в динамическом SQL:
Показатель
Для каждого из этих решений простой (по умолчанию) индекс btree range
будет способствовать производительности в больших таблицах:
CREATE INDEX foo on test (range);
Индекс btree имеет ограниченное использование для типов диапазонов , но мы можем получать предварительно отсортированные данные и, возможно, даже сканирование только по индексу.