Annahmen / Erläuterungen
Es ist nicht erforderlich, zwischen einer infinity
oberen Schranke und einer offenen oberen Schranke zu unterscheiden ( upper(range) IS NULL
). (Sie können es so oder so haben, aber auf diese Weise ist es einfacher.)
Da date
es sich um einen diskreten Typ handelt, haben alle Bereiche Standardgrenzen [)
.
Per Dokumentation:
Die Einbau-Bereichstypen int4range
, int8range
und die daterange
gesamte Nutzung eine kanonische Form , die die untere Grenze , und umfasst nicht die obere Grenze enthält; das heißt [)
,.
Für andere Typen (wie tsrange
!) Würde ich dasselbe nach Möglichkeit durchsetzen:
Lösung mit reinem SQL
Mit CTEs zur Klarheit:
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;
Oder , wie bei Unterabfragen, schneller, aber nicht so einfach zu lesen:
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;
Oder mit einer Unterabfrageebene weniger, aber umgekehrter Sortierreihenfolge:
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;
- Sortieren Sie das Fenster im zweiten Schritt mit
ORDER BY range DESC NULLS LAST
(mit NULLS LAST
), um eine perfekt umgekehrte Sortierreihenfolge zu erhalten. Dies sollte billiger (einfacher herzustellen, passt perfekt zur Sortierreihenfolge des vorgeschlagenen Index) und für Eckfälle mit genau seinrank IS NULL
.
Erklären
a
: range
Berechnen Sie während der Bestellung nach das laufende Maximum der oberen Schranke ( enddate
) mit einer Fensterfunktion.
Ersetzen Sie NULL-Grenzen (unbegrenzt) durch +/- infinity
, um dies zu vereinfachen (keine speziellen NULL-Fälle).
b
: In der gleichen Sortierreihenfolge, wenn die vorherige enddate
früher ist als startdate
wir eine Lücke haben und einen neuen Bereich beginnen ( step
).
Denken Sie daran, dass die Obergrenze immer ausgeschlossen ist.
c
: Bilden Sie Gruppen ( grp
), indem Sie Schritte mit einer anderen Fensterfunktion zählen.
Im äußeren SELECT
Build reicht die Unter- bis Obergrenze in jeder Gruppe. Voilá.
Eng verwandte Antwort auf SO mit mehr Erklärung:
Verfahrenslösung mit plpgsql
Funktioniert für jeden Tabellen- / Spaltennamen, jedoch nur für Typ daterange
.
Prozedurale Lösungen mit Schleifen sind normalerweise langsamer, aber in diesem speziellen Fall erwarte ich, dass die Funktion wesentlich schneller ist, da sie nur einen einzigen sequentiellen Scan benötigt :
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;
Anruf:
SELECT * FROM f_range_agg('test', 'range'); -- table and column name
Die Logik ist ähnlich wie bei den SQL-Lösungen, aber wir können mit einem einzigen Durchgang auskommen.
SQL-Geige.
Verbunden:
Der übliche Drill zum Behandeln von Benutzereingaben in dynamischem SQL:
Index
Für jede dieser Lösungen wäre ein einfacher (Standard-) Btree-Index range
für die Leistung in großen Tabellen von entscheidender Bedeutung:
CREATE INDEX foo on test (range);
Ein Btree-Index ist für Bereichstypen von begrenztem Nutzen , aber wir können vorsortierte Daten und möglicherweise sogar einen Nur-Index-Scan abrufen.