Presupposti / Chiarimenti
Non è necessario distinguere tra infinitye aprire il limite superiore ( upper(range) IS NULL). (Puoi averlo in entrambi i modi, ma è più semplice in questo modo.)
Poiché dateè un tipo discreto, tutti gli intervalli hanno [)limiti predefiniti .
Per documentazione:
Il built-in tipi di intervallo int4range, int8rangee daterangetutto l'uso una forma canonica che comprende il limite inferiore ed esclude il limite superiore; cioè [).
Per altri tipi (come tsrange!), Imporrei lo stesso, se possibile:
Soluzione con SQL puro
Con CTE per chiarezza:
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;
Oppure , lo stesso con le subquery, più veloce ma meno facile da leggere:
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;
O con un livello di subquery in meno, ma capovolgendo l'ordinamento:
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;
- Ordinare la finestra nel secondo passaggio con
ORDER BY range DESC NULLS LAST(con NULLS LAST) per ottenere un ordinamento perfettamente invertito. Questo dovrebbe essere più economico (più facile da produrre, corrisponde perfettamente all'ordinamento dell'indice suggerito) e preciso per i casi angolari con rank IS NULL.
Spiegare
a: Durante l'ordine range, calcola il massimo corrente del limite superiore ( enddate) con una funzione di finestra.
Sostituisci i limiti NULL (senza limiti) con +/- infinitysolo per semplificare (nessun caso NULL speciale).
b: Nello stesso ordinamento, se il precedente enddateè precedente a startdateabbiamo un gap e iniziamo un nuovo intervallo ( step).
Ricorda, il limite superiore è sempre escluso.
c: Forma gruppi ( grp) contando i passaggi con un'altra funzione di finestra.
Nella SELECTbuild esterna va dal limite inferiore a quello superiore in ciascun gruppo. Ecco.
Risposta strettamente correlata su SO con ulteriori spiegazioni:
Soluzione procedurale con plpgsql
Funziona con qualsiasi nome di tabella / colonna, ma solo per il tipo daterange.
Le soluzioni procedurali con loop sono in genere più lente, ma in questo caso speciale mi aspetto che la funzione sia sostanzialmente più veloce poiché richiede solo una singola scansione sequenziale :
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;
Chiamata:
SELECT * FROM f_range_agg('test', 'range'); -- table and column name
La logica è simile alle soluzioni SQL, ma possiamo accontentarci di un singolo passaggio.
SQL Fiddle.
Relazionato:
Il solito drill per gestire l'input dell'utente in SQL dinamico:
Indice
Per ognuna di queste soluzioni un indice btree semplice (predefinito) su rangesarebbe strumentale per le prestazioni in grandi tavoli:
CREATE INDEX foo on test (range);
Un indice btree è di utilità limitata per i tipi di intervallo , ma possiamo ottenere dati preordinati e forse anche una scansione di soli indici.