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

Tabla de historial de acciones promedio

La dificultad especial de esta tarea:no puede simplemente elegir puntos de datos dentro de su intervalo de tiempo, sino que debe considerar los últimos punto de datos antes el rango de tiempo y el primero punto de datos después el rango de tiempo adicionalmente. Esto varía para cada fila y cada punto de datos puede o no existir. Requiere una consulta sofisticada y dificulta el uso de índices.

Podría usar tipos de rango y operadores (Postgres 9.2+ ) para simplificar los cálculos:

WITH input(a,b) AS (SELECT '2013-01-01'::date  -- your time frame here
                         , '2013-01-15'::date) -- inclusive borders
SELECT store_id, product_id
     , sum(upper(days) - lower(days))                    AS days_in_range
     , round(sum(value * (upper(days) - lower(days)))::numeric
                    / (SELECT b-a+1 FROM input), 2)      AS your_result
     , round(sum(value * (upper(days) - lower(days)))::numeric
                    / sum(upper(days) - lower(days)), 2) AS my_result
FROM (
   SELECT store_id, product_id, value, s.day_range * x.day_range AS days
   FROM  (
      SELECT store_id, product_id, value
           , daterange (day, lead(day, 1, now()::date)
             OVER (PARTITION BY store_id, product_id ORDER BY day)) AS day_range 
      FROM   stock
      ) s
   JOIN  (
      SELECT daterange(a, b+1) AS day_range
      FROM   input
      ) x ON s.day_range && x.day_range
   ) sub
GROUP  BY 1,2
ORDER  BY 1,2;

Tenga en cuenta que uso el nombre de columna day en lugar de date . Nunca uso nombres de tipos básicos como nombres de columna.

En la subconsulta sub Obtengo el día de la siguiente fila para cada elemento con la función de ventana lead() , usando la opción incorporada para proporcionar "hoy" como predeterminado donde no hay una fila siguiente.
Con esto formo un daterange y compararlo con la entrada con el operador de superposición && , calculando el intervalo de fechas resultante con el operador de intersección * .

Todos los rangos aquí son con exclusivo borde superior Es por eso que agrego un día al rango de entrada. De esta manera podemos simplemente restar lower(range) desde upper(range) para obtener el número de días.

Supongo que "ayer" es el último día con datos confiables. "Hoy" todavía puede cambiar en una aplicación de la vida real. En consecuencia, uso "hoy" (now()::date ) como borde superior exclusivo para rangos abiertos.

Proporciono dos resultados:

  • your_result está de acuerdo con los resultados mostrados.
    Dividirás por el número de días en tu intervalo de fechas incondicionalmente. Por ejemplo, si un artículo solo aparece en la lista del último día, obtendrá un "promedio" muy bajo (¡engañoso!).

  • my_result calcula números iguales o superiores.
    Divido por el real número de días que un artículo está en la lista. Por ejemplo, si un artículo solo aparece en la lista el último día, devuelvo el valor de la lista como promedio.

Para dar sentido a la diferencia, agregué la cantidad de días en que se incluyó el artículo:days_in_range

SQL Fiddle .

Índice y rendimiento

Para este tipo de datos, las filas antiguas normalmente no cambian. Esto sería un excelente caso para una vista materializada :

CREATE MATERIALIZED VIEW mv_stock AS
SELECT store_id, product_id, value
     , daterange (day, lead(day, 1, now()::date) OVER (PARTITION BY store_id, product_id
                                                       ORDER BY day)) AS day_range
FROM   stock;

Luego puede agregar un índice GiST que admita el operador relevante && :

CREATE INDEX mv_stock_range_idx ON mv_stock USING gist (day_range);

Gran caso de prueba

Realicé una prueba más realista con 200k filas. La consulta que utilizó el MV fue aproximadamente 6 veces más rápida, lo que a su vez fue ~ 10 veces más rápida que la consulta de @Joop. El rendimiento depende en gran medida de la distribución de datos. Un MV ayuda más con mesas grandes y alta frecuencia de entradas. Además, si la tabla tiene columnas que no son relevantes para esta consulta, un MV puede ser más pequeño. Una cuestión de costo versus ganancia.

Puse todas las soluciones publicadas hasta ahora (y adaptadas) en un gran violín para jugar:

SQL Fiddle con gran caso de prueba.
SQL Fiddle con solo 40k filas - para evitar el tiempo de espera en sqlfiddle.com