sql >> Base de Datos >  >> RDS >> PostgreSQL

Encuentre y sume rangos de fechas con registros superpuestos en postgresql

demostración:db<>fiddle (utiliza el conjunto de datos anterior con la parte A-B superpuesta)

Descargo de responsabilidad: Esto funciona para intervalos de días, no para marcas de tiempo. El requisito de ts llegó más tarde.

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 genera todas las fechas entre el inicio y el final. Entonces, cada fecha en que existe una actividad obtiene una fila con el count específico
  2. Agrupación de todas las fechas, agregación de todas las actividades existentes y suma de sus recuentos
  3. HAVING filtra las fechas en las que solo existe una actividad
  4. Debido a que hay diferentes días con las mismas actividades, solo necesitamos un representante:filtrar todos los duplicados con DISTINCT ON
  5. Combina este resultado con la tabla original para obtener el inicio y el final. (tenga en cuenta que "fin" es una palabra reservada en Postgres, ¡mejor debería encontrar otro nombre de columna!). Era más cómodo perderlos antes, pero es posible obtener estos datos dentro de la subconsulta.
  6. Agrupe esta unión para obtener la fecha más temprana y más reciente de cada intervalo.

Aquí hay una versión para las marcas de tiempo:

demostración: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

La idea principal es identificar posibles franjas horarias. Así que tomo cada tiempo conocido (tanto el inicio como el final) y los pongo en una lista ordenada. Entonces puedo tomar los primeros dos tiempos conocidos (17:00 desde el inicio A y 18:00 desde el inicio B) y verificar qué intervalo está en él. Luego compruebo el 2.° y el 3.°, luego el 3.° y el 4.° y así sucesivamente.

En el primer intervalo de tiempo sólo cabe A. En el segundo de 18-19 también encaja B. En el siguiente slot 19-20 también C, del 20 al 20:30 ya no cabe A, solo B y C. El siguiente es 20:30-22 donde solo cabe B, finalmente se añade 22-23 D B y, por último, pero no menos importante, solo D encaja en 23-23:30.

Así que tomo esta lista de tiempo y la combino con la tabla de actividades donde se cruzan los intervalos. Después de eso, es solo una agrupación por intervalo de tiempo y un resumen de su cuenta.

  1. esto pone ambos ts de una fila en una matriz cuyos elementos se expanden en una fila por elemento con unnest . Así que tengo todos los tiempos en una columna que se puede ordenar simplemente
  2. utilizando la función de ventana permite tomar el valor de la siguiente fila en la actual. Entonces puedo crear un rango de marca de tiempo a partir de estos dos valores con tsrange
  3. Este filtro es necesario porque la última fila no tiene "valor siguiente". Esto crea un NULL valor que es interpretado por tsrange como infinito. Así que esto crearía una increíble franja horaria equivocada. Entonces necesitamos filtrar esta fila.
  4. Únete a las franjas horarias contra la mesa original. El && el operador verifica si dos tipos de rango se superponen.
  5. Agrupación por intervalos de tiempo únicos, agregando los nombres y el conteo. Filtre los intervalos de tiempo con una sola actividad usando el HAVING cláusula
  6. Es un poco complicado obtener los puntos de inicio y final correctos. Entonces, los puntos de inicio son el máximo del inicio de la actividad o el comienzo de un intervalo de tiempo (que se puede obtener usando lower ). P.ej. Tome la franja 20-20:30:Comienza a las 20h pero ni B ni C tienen su punto de partida allí. Similar a la hora de finalización.