PostgreSQL
 sql >> Base de Dados >  >> RDS >> PostgreSQL

Localizar e somar intervalos de datas com registros sobrepostos no postgresql


demo:db<>fiddle (usa o conjunto de dados antigo com a parte A-B sobreposta)

Isenção de responsabilidade: Isso funciona para intervalos de dias, não para carimbos de data/hora. A exigência de ts veio depois.
SELECT
    s.acts,
    s.sum,
    MIN(a.start) as start,
    MAX(a.end) as end
FROM (
    SELECT DISTINCT ON (acts)
        array_agg(name) as acts,
        SUM(count)
    FROM
        activities, generate_series(start, "end", interval '1 day') gs
    GROUP BY gs
    HAVING cardinality(array_agg(name)) > 1
) s
JOIN activities a
ON a.name = ANY(s.acts)
GROUP BY s.acts, s.sum
  1. generate_series gera todas as datas entre o início e o fim. Portanto, cada data em que uma atividade existe recebe uma linha com a count específica
  2. Agrupando todas as datas, agregando todas as atividades existentes e somando suas contagens
  3. HAVING filtra as datas em que existe apenas uma atividade
  4. Como há dias diferentes com as mesmas atividades, precisamos apenas de um representante:Filtre todas as duplicatas com DISTINCT ON
  5. Junte este resultado com a tabela original para obter o início e o fim. (note que "end" é uma palavra reservada no Postgres, é melhor você encontrar outro nome de coluna!). Antes era mais confortável perdê-los, mas é possível obter esses dados na subconsulta.
  6. Agrupe esta junção para obter a data mais cedo e mais recente de cada intervalo.

Aqui está uma versão para carimbos de data/hora:

demo:db<>fiddle
WITH timeslots AS (
    SELECT * FROM (
        SELECT
            tsrange(timepoint, lead(timepoint) OVER (ORDER BY timepoint)),
            lead(timepoint) OVER (ORDER BY timepoint)     -- 2
        FROM (
            SELECT 
                unnest(ARRAY[start, "end"]) as timepoint  -- 1 
            FROM
                activities
            ORDER BY timepoint
        ) s
    )s  WHERE lead IS NOT NULL                            -- 3
)
SELECT 
    GREATEST(MAX(start), lower(tsrange)),                 -- 6
    LEAST(MIN("end"), upper(tsrange)),
    array_agg(name),                                      -- 5
    sum(count)
FROM 
    timeslots t
JOIN activities a
ON t.tsrange && tsrange(a.start, a.end)                   -- 4
GROUP BY tsrange
HAVING cardinality(array_agg(name)) > 1

A ideia principal é identificar possíveis intervalos de tempo. Então, eu pego todos os tempos conhecidos (início e fim) e os coloco em uma lista ordenada. Assim posso pegar os primeiros tempos conhecidos de reboque (17:00 da largada A e 18:00 da largada B) e verificar qual intervalo está nele. Então eu verifico para o 2º e 3º, depois para o 3º e 4º e assim por diante.

No primeiro intervalo de tempo apenas A se encaixa. No segundo de 18-19 também B é apropriado. No próximo slot 19-20 também C, das 20 às 20:30 A não cabe mais, apenas B e C. O próximo é 20:30-22 onde cabe apenas B, finalmente 22-23 D é adicionado a B e por último mas não menos importante apenas D se encaixa em 23-23:30.

Então eu pego essa lista de tempo e a junto com a tabela de atividades onde os intervalos se cruzam. Depois disso, é apenas um agrupamento por intervalo de tempo e soma sua contagem.
  1. isso coloca os dois ts de uma linha em uma matriz cujos elementos são expandidos em uma linha por elemento com unnest . Então eu coloco todos os tempos em uma coluna que pode ser simplesmente ordenada
  2. usando a função de janela principal permite levar o valor da próxima linha para a atual. Assim, posso criar um intervalo de carimbo de data/hora desses dois valores com tsrange
  3. Este filtro é necessário porque a última linha não tem "próximo valor". Isso cria um NULL valor que é interpretado por tsrange como infinito. Portanto, isso criaria um incrível intervalo de tempo errado. Portanto, precisamos filtrar essa linha.
  4. Junte os intervalos de tempo à tabela original. O && operador verifica se dois tipos de intervalo se sobrepõem.
  5. Agrupamento por intervalos de tempo únicos, agregando os nomes e a contagem. Filtre os intervalos de tempo com apenas uma atividade usando o HAVING cláusula
  6. Um pouco complicado para obter os pontos iniciais e finais corretos. Portanto, os pontos de início são o máximo do início da atividade ou o início de um intervalo de tempo (que pode ser obtido usando lower ). Por exemplo. Pegue o slot 20-20:30:Começa às 20h, mas nem B nem C tem seu ponto de partida lá. Semelhante ao horário de término.