Pressupostos / Esclarecimentos
Não é necessário diferenciar entre infinity
e abrir o limite superior ( upper(range) IS NULL
). (Você pode escolher de qualquer maneira, mas é mais simples assim.)
Como date
é um tipo discreto, todos os intervalos têm [)
limites padrão .
Por documentação:
O embutido tipos de intervalo int4range
, int8range
e daterange
todo o uso de uma forma canónica que inclui o limite inferior e o limite superior exclui; isto é, [)
.
Para outros tipos (como tsrange
!) Eu aplicaria o mesmo, se possível:
Solução com SQL puro
Com CTEs para maior clareza:
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;
Ou , o mesmo com subconsultas, mais rápido, mas menos fácil, também leia:
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;
Ou com menos um nível de subconsulta, mas invertendo a ordem de classificação:
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;
- Classifique a janela na segunda etapa com
ORDER BY range DESC NULLS LAST
(com NULLS LAST
) para obter a ordem de classificação perfeitamente invertida. Isso deve ser mais barato (mais fácil de produzir, corresponde perfeitamente à ordem de classificação do índice sugerido) e preciso para os casos de canto rank IS NULL
.
Explicar
a
: Ao fazer o pedido range
, calcule o máximo em execução do limite superior ( enddate
) com uma função de janela.
Substitua limites NULL (sem limites) por +/- infinity
apenas para simplificar (sem casos NULL especiais).
b
: Na mesma ordem de classificação, se o anterior enddate
for anterior startdate
, temos uma lacuna e iniciaremos um novo intervalo ( step
).
Lembre-se, o limite superior é sempre excluído.
c
: Forme grupos ( grp
) contando as etapas com outra função da janela.
Na SELECT
construção externa varia do limite inferior ao superior em cada grupo. Voilá.
Resposta intimamente relacionada ao SO, com mais explicações:
Solução processual com plpgsql
Funciona para qualquer nome de tabela / coluna, mas apenas para o tipo daterange
.
As soluções procedurais com loops são tipicamente mais lentas, mas neste caso especial, espero que a função seja substancialmente mais rápida, pois precisa apenas de uma única varredura seqüencial :
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;
Ligar:
SELECT * FROM f_range_agg('test', 'range'); -- table and column name
A lógica é semelhante às soluções SQL, mas podemos nos contentar com uma única passagem.
SQL Fiddle.
Relacionado:
O drill usual para manipular a entrada do usuário no SQL dinâmico:
Índice
Para cada uma dessas soluções, um índice btree simples (padrão) range
seria fundamental para o desempenho em grandes tabelas:
CREATE INDEX foo on test (range);
Um índice btree é de uso limitado para tipos de intervalo , mas podemos obter dados pré-classificados e talvez até uma varredura apenas de índice.