Estou tentando combinar vários períodos (minha carga é de aproximadamente 500, na maioria dos casos 10) que podem ou não se sobrepor aos maiores períodos possíveis contíguos. 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 | ==
--+---------------------------
| ====== == ===============>
postgresql
aggregate
range-types
Villiers Strauss
fonte
fonte
Respostas:
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.)infinity
nos tipos de intervalo do PostgreSQLComo
date
é um tipo discreto, todos os intervalos têm[)
limites padrão . Por documentação: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, também leia:
Ou com menos um nível de subconsulta, mas invertendo a ordem de classificação:
ORDER BY range DESC NULLS LAST
(comNULLS 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 cantorank IS NULL
.Explicar
a
: Ao fazer o pedidorange
, 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 anteriorenddate
for anteriorstartdate
, 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 :
Ligar:
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:Um índice btree é de uso limitado para tipos de intervalo , mas podemos obter dados pré-classificados e talvez até uma varredura apenas de índice.
fonte
EXPLAIN ( ANALYZE, TIMING OFF)
e comparar melhor de cinco.max(COALESCE(upper(range), 'infinity')) OVER (ORDER BY range) AS enddate
serve (CTE A) ? Não pode ser justoCOALESCE(upper(range), 'infinity') as enddate
? AFAIKmax() + over (order by range)
retornaráupper(range)
aqui.Eu vim com isso:
Ainda precisa de um pouco de aprimoramento, mas a ideia é a seguinte:
+
) falhar, retorne o intervalo já construído e reinicializefonte
generate_series()
para cada linha, especialmente se não pode haver escalas abertas ...Há alguns anos, testei soluções diferentes (entre outras semelhantes às do @ErwinBrandstetter) para mesclar períodos sobrepostos em um sistema Teradata e achei o mais eficiente (usando Funções analíticas, a versão mais recente do Teradata possui funções internas para essa tarefa).
maxEnddate
maxEnddate
usa a próxima linhaLEAD
e está quase pronto. Somente para a última linhaLEAD
retorna aNULL
, para resolver isso, calcule a data final máxima de todas as linhas de uma partição na etapa 2 eCOALESCE
nela.Por que foi mais rápido? Dependendo dos dados reais, a etapa 2 pode reduzir bastante o número de linhas; portanto, a próxima etapa precisa operar apenas em um pequeno subconjunto, além de remover 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 de desempenho reais.
fonte
Por diversão, eu tentei. Eu achei que esse era 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 a primeira daterange. Dica
+
é uma união de intervalo no contexto de intervalos.Então a usamos assim,
fonte
('2015-01-01', '2015-01-03'), ('2015-01-03', '2015-01-05'), ('2015-01-05', '2015-01-06')
.