Estou tentando combinar vários intervalos de datas (minha carga é de cerca de 500 no máximo, na maioria dos casos 10) que podem ou não se sobrepor nos maiores intervalos de datas contíguos possíveis. Por exemplo:
Dados:
CREATE TABLE test (
id SERIAL PRIMARY KEY NOT NULL,
range DATERANGE
);
INSERT INTO test (range) VALUES
(DATERANGE('2015-01-01', '2015-01-05')),
(DATERANGE('2015-01-01', '2015-01-03')),
(DATERANGE('2015-01-03', '2015-01-06')),
(DATERANGE('2015-01-07', '2015-01-09')),
(DATERANGE('2015-01-08', '2015-01-09')),
(DATERANGE('2015-01-12', NULL)),
(DATERANGE('2015-01-10', '2015-01-12')),
(DATERANGE('2015-01-10', '2015-01-12'));
A tabela se parece com:
id | range
----+-------------------------
1 | [2015-01-01,2015-01-05)
2 | [2015-01-01,2015-01-03)
3 | [2015-01-03,2015-01-06)
4 | [2015-01-07,2015-01-09)
5 | [2015-01-08,2015-01-09)
6 | [2015-01-12,)
7 | [2015-01-10,2015-01-12)
8 | [2015-01-10,2015-01-12)
(8 rows)
Resultados desejados:
combined
--------------------------
[2015-01-01, 2015-01-06)
[2015-01-07, 2015-01-09)
[2015-01-10, )
Representação visual:
1 | =====
2 | ===
3 | ===
4 | ==
5 | =
6 | =============>
7 | ==
8 | ==
--+---------------------------
| ====== == ===============>
Suposições / Esclarecimentos
infinity
e abrir o limite superior (upper(range) IS NULL
). (Você pode ter de qualquer maneira, mas é mais simples assim.)infinity
nos tipos de intervalo do PostgreSQLdate
é um tipo discreto, todos os intervalos têm[)
limites padrão. O manual:Para outros tipos (como
tsrange
!) Eu aplicaria o mesmo, se possível:Solução com SQL puro
Com CTEs para maior clareza:
Ou , o mesmo com subconsultas, mais rápido, mas menos fácil de ler:
Como?
a
: Ao ordenar porrange
, calcule o máximo em execução do limite superior (enddate
) com uma função de janela.Substitua os limites NULL (ilimitados) por +/-
infinity
apenas para simplificar (sem casos NULL especiais).b
: Na mesma ordem de classificação, se o anteriorenddate
for anteriorstartdate
, temos uma lacuna e iniciamos um novo intervalo (step
).Lembre-se, o limite superior é sempre excluído.
c
: Forme grupos (grp
) contando passos com outra função de janela.Na construção externa
SELECT
varia do limite inferior ao superior em cada grupo. Voilá.Ou com um nível de subconsulta a menos, mas invertendo a ordem de classificação:
Classifique a janela na segunda etapa com
ORDER BY range DESC NULLS LAST
(withNULLS LAST
) para obter uma 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 casos de canto comrank IS NULL
. Ver:Resposta relacionada com mais explicações:
Solução processual com plpgsql
Funciona para qualquer nome de tabela/coluna, mas apenas para o tipo
daterange
.Soluções procedurais com loops são normalmente mais lentas, mas neste caso especial, espero que a função seja substancialmente mais rápida , pois precisa apenas de uma única varredura sequencial :
Ligar:
A lógica é semelhante às soluções SQL, mas podemos nos contentar com uma única passagem.
SQL Fiddle.
Relacionado:
O exercício usual para lidar com a entrada do usuário em SQL dinâmico:
Índice
Para cada uma dessas soluções, um índice btree simples (padrão)
range
seria fundamental para o desempenho em tabelas grandes:Um índice btree é de uso limitado para tipos de intervalo , mas podemos obter dados pré-classificados e talvez até mesmo uma varredura somente de índice.
Eu vim com isso:
Ainda falta um pouco de afiação, mas a ideia é a seguinte:
+
) falhar, retorne o intervalo já construído e reinicieAlguns anos atrás, testei diferentes soluções (entre outras, algumas semelhantes às de @ErwinBrandstetter) para mesclar períodos sobrepostos em um sistema Teradata e achei a seguinte a mais eficiente (usando funções analíticas, a versão mais recente do Teradata possui funções integradas para essa tarefa).
maxEnddate
maxEnddate
usa a próxima linhaLEAD
e está quase pronto. Somente para a última linhaLEAD
retorna umNULL
, para resolver isso, calcule a data final máxima de todas as linhas de uma partição na etapa 2 eCOALESCE
ela.Por que foi mais rápido? Dependendo dos dados reais, a etapa nº 2 pode reduzir bastante o número de linhas, portanto, a próxima etapa precisa operar apenas em um pequeno subconjunto; além disso, remove a agregação.
violino
Como isso foi mais rápido no Teradata, não sei se é o mesmo para o PostgreSQL, seria bom obter alguns números reais de desempenho.
Para me divertir, dei uma chance. Achei esse o método mais rápido e limpo para fazer isso. Primeiro definimos uma função que mescla se houver uma sobreposição ou se as duas entradas forem adjacentes, se não houver sobreposição ou adjacência, simplesmente retornamos o primeiro intervalo de datas. Hint
+
é uma união de intervalo no contexto de intervalos.Então usamos assim,