Suma continua / recuento / promedio sobre el intervalo de fecha

20

En una base de datos de transacciones que abarca miles de entidades durante 18 meses, me gustaría ejecutar una consulta para agrupar cada período de 30 días posible entity_idcon una SUMA de los montos de sus transacciones y COUNT de sus transacciones en ese período de 30 días, y devolver los datos de una manera que luego puedo consultar. Después de muchas pruebas, este código logra mucho de lo que quiero:

SELECT id, trans_ref_no, amount, trans_date, entity_id,
    SUM(amount) OVER(PARTITION BY entity_id, date_trunc('month',trans_date) ORDER BY entity_id, trans_date ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS trans_total,
    COUNT(id)   OVER(PARTITION BY entity_id, date_trunc('month',trans_date) ORDER BY entity_id, trans_date ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS trans_count
  FROM transactiondb;

Y usaré en una consulta más grande estructurada algo como:

SELECT * FROM (
  SELECT id, trans_ref_no, amount, trans_date, entity_id,
      SUM(amount) OVER(PARTITION BY entity_id, date_trunc('month',trans_date) ORDER BY entity_id, trans_date ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS trans_total,
      COUNT(id)   OVER(PARTITION BY entity_id, date_trunc('month',trans_date) ORDER BY entity_id, trans_date ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS trans_count
    FROM transactiondb ) q
WHERE trans_count >= 4
AND trans_total >= 50000;

El caso que esta consulta no cubre es cuando los recuentos de transacciones abarcarían varios meses, pero aún se realizarían dentro de los 30 días de diferencia. ¿Es posible este tipo de consulta con Postgres? Si es así, agradezco cualquier aporte. Muchos de los otros temas discuten agregados "en ejecución ", no rodantes .

Actualizar

El CREATE TABLEguión:

CREATE TABLE transactiondb (
    id integer NOT NULL,
    trans_ref_no character varying(255),
    amount numeric(18,2),
    trans_date date,
    entity_id integer
);

Los datos de muestra se pueden encontrar aquí . Estoy ejecutando PostgreSQL 9.1.16.

La producción ideal incluiría SUM(amount)y COUNT()de todas las transacciones durante un período continuo de 30 días. Ver esta imagen, por ejemplo:

Ejemplo de filas que idealmente se incluirían en un "conjunto" pero no porque mi conjunto sea estático por mes.

El resaltado de fecha verde indica lo que incluye mi consulta. El resaltado de la fila amarilla indica registros de lo que me gustaría formar parte del conjunto.

Lectura previa:

Tufelkinder
fuente
1
¿ every possible 30-day period by entity_idQuieres decir que el período puede comenzar cualquier día, entonces 365 períodos posibles en un año (no bisiesto)? ¿O solo desea considerar los días con una transacción real como inicio de un período individualmente para cualquiera entity_id ? De cualquier manera, proporcione la definición de la tabla, la versión de Postgres, algunos datos de muestra y el resultado esperado para la muestra.
Erwin Brandstetter
En teoría, quise decir cualquier día, pero en la práctica no hay necesidad de considerar días en los que no hay transacciones. He publicado los datos de muestra y la definición de la tabla.
tufelkinder
Por lo tanto, desea acumular filas de la misma entity_iden una ventana de 30 días a partir de cada transacción real. ¿Puede haber múltiples transacciones para el mismo (trans_date, entity_id)o es esa combinación definida única? La definición de su tabla no tiene UNIQUErestricciones o PK, pero parece que faltan restricciones ...
Erwin Brandstetter
La única restricción está en idla clave primaria. Puede haber múltiples transacciones por entidad por día.
tufelkinder
Acerca de la distribución de datos: ¿hay entradas (por entidad_id) para la mayoría de los días?
Erwin Brandstetter

Respuestas:

26

La consulta que tienes

Puede simplificar su consulta usando una WINDOWcláusula, pero eso solo acorta la sintaxis, no cambia el plan de consulta.

SELECT id, trans_ref_no, amount, trans_date, entity_id
     , SUM(amount) OVER w AS trans_total
     , COUNT(*)    OVER w AS trans_count
FROM   transactiondb
WINDOW w AS (PARTITION BY entity_id, date_trunc('month',trans_date)
             ORDER BY trans_date
             ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING);
  • También se usa un poco más rápido count(*), ya idque sin duda se define NOT NULL?
  • Y no es necesario ORDER BY entity_idya que yaPARTITION BY entity_id

Sin embargo, puede simplificar aún más:
no agregue nada ORDER BYa la definición de ventana, no es relevante para su consulta. Entonces no necesita definir un marco de ventana personalizado, tampoco:

SELECT id, trans_ref_no, amount, trans_date, entity_id
     , SUM(amount) OVER w AS trans_total
     , COUNT(*)    OVER w AS trans_count
FROM   transactiondb
WINDOW w AS (PARTITION BY entity_id, date_trunc('month',trans_date);

Más simple, más rápido, pero aún así es una mejor versión de lo que tienes , con meses estáticos .

La consulta que quieras

... no está claramente definido, así que me basaré en estos supuestos:

Cuente las transacciones y la cantidad por cada período de 30 días dentro de la primera y última transacción de cualquiera entity_id. Excluya los períodos iniciales y finales sin actividad, pero incluya todos los períodos posibles de 30 días dentro de esos límites externos.

SELECT entity_id, trans_date
     , COALESCE(sum(daily_amount) OVER w, 0) AS trans_total
     , COALESCE(sum(daily_count)  OVER w, 0) AS trans_count
FROM  (
   SELECT entity_id
        , generate_series (min(trans_date)::timestamp
                         , GREATEST(min(trans_date), max(trans_date) - 29)::timestamp
                         , interval '1 day')::date AS trans_date
   FROM   transactiondb 
   GROUP  BY 1
   ) x
LEFT JOIN (
   SELECT entity_id, trans_date
        , sum(amount) AS daily_amount, count(*) AS daily_count
   FROM   transactiondb
   GROUP  BY 1, 2
   ) t USING (entity_id, trans_date)
WINDOW w AS (PARTITION BY entity_id ORDER BY trans_date
             ROWS BETWEEN CURRENT ROW AND 29 FOLLOWING);

Esto enumera todos los períodos de 30 días para cada uno entity_idcon sus agregados y con trans_dateel primer día (incluido) del período. Para obtener valores para cada fila individual, únase a la tabla base una vez más ...

La dificultad básica es la misma que se analiza aquí:

La definición de marco de una ventana no puede depender de los valores de la fila actual.

Y más bien llame generate_series()con timestampentrada:

La consulta que realmente quieres

Después de la actualización y discusión de la pregunta:
acumule filas de la misma entity_iden una ventana de 30 días a partir de cada transacción real.

Dado que sus datos se distribuyen escasamente, debería ser más eficiente ejecutar una autounión con una condición de rango , sobre todo porque Postgres 9.1 aún no tiene LATERALuniones:

SELECT t0.id, t0.amount, t0.trans_date, t0.entity_id
     , sum(t1.amount) AS trans_total, count(*) AS trans_count
FROM   transactiondb t0
JOIN   transactiondb t1 USING (entity_id)
WHERE  t1.trans_date >= t0.trans_date
AND    t1.trans_date <  t0.trans_date + 30  -- exclude upper bound
-- AND    t0.entity_id = 114284  -- or pick a single entity ...
GROUP  BY t0.id  -- is PK!
ORDER  BY t0.trans_date, t0.id

SQL Fiddle.

Una ventana móvil solo podría tener sentido (con respecto al rendimiento) con datos para la mayoría de los días.

Esto no agrega duplicados (trans_date, entity_id)por día, pero todas las filas del mismo día siempre se incluyen en la ventana de 30 días.

Para una tabla grande, un índice de cobertura como este podría ayudar bastante:

CREATE INDEX transactiondb_foo_idx
ON transactiondb (entity_id, trans_date, amount);

La última columna amountsolo es útil si obtiene escaneos de solo índice. De lo contrario, déjalo caer.

Pero de todos modos no se usará mientras selecciona toda la tabla. Soportaría consultas para un pequeño subconjunto.

Erwin Brandstetter
fuente
Esto se ve muy bien, para ello, en los datos ahora, y tratando de entender todo lo que su consulta está haciendo realidad ...
tufelkinder
@tufelkinder: se agregó una solución para la pregunta actualizada.
Erwin Brandstetter
Revisándolo ahora. Estoy intrigado de que se ejecute en SQL Fiddle ... Cuando intento ejecutarlo directamente en mi transaccióndb, se column "t0.amount" must appear in the GROUP BY clause...
produce
@tufelkinder: reduje el caso de prueba a 100 filas. sqlfiddle limita el tamaño de los datos de prueba. Jake (el autor) redujo el límite de límites hace un par de meses, por lo que el sitio se detiene con menos facilidad.
Erwin Brandstetter
1
Perdón por el retraso, necesitaba probarlo en la base de datos completa. Su respuesta fue magníficamente profunda y educativa, como siempre. ¡Gracias!
tufelkinder