Presupposti / Chiarimenti
Non è necessario distinguere tra infinity
e 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
, int8range
e daterange
tutto 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 +/- infinity
solo per semplificare (nessun caso NULL speciale).
b
: Nello stesso ordinamento, se il precedente enddate
è precedente a startdate
abbiamo 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 SELECT
build 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 range
sarebbe 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.