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

Funciones de ventana o expresiones de tabla comunes:cuente filas anteriores dentro del rango

No creo que pueda hacer esto a bajo costo con una consulta simple, CTE y funciones de ventana:su definición de marco es estática, pero necesita un marco dinámico (dependiendo de los valores de la columna).

En general, deberá definir los límites inferior y superior de su ventana con cuidado:las siguientes consultas excluir la fila actual e incluir el borde inferior.
Todavía hay una diferencia menor:la función incluye pares anteriores de la fila actual, mientras que la subconsulta correlacionada los excluye...

Caso de prueba

Usando ts en lugar de la palabra reservada date como nombre de columna.

CREATE TABLE test (
  id  bigint
, ts  timestamp
);

ROM - Consulta de Roman

Use CTE, agregue marcas de tiempo en una matriz, anide, cuente...
Si bien es correcto, el rendimiento deteriora drásticamente con más de un puñado de filas. Hay un par de asesinos de rendimiento aquí. Ver más abajo.

ARR - cuenta los elementos de la matriz

Tomé la consulta de Roman y traté de simplificarla un poco:

  • Eliminar el segundo CTE que no es necesario.
  • Transforme la primera CTE en una subconsulta, que es más rápida.
  • Directo count() en lugar de volver a agregar en una matriz y contar con array_length() .

Pero el manejo de arreglos es costoso y el rendimiento todavía deteriora gravemente con más filas.

SELECT id, ts
     , (SELECT count(*)::int - 1
        FROM   unnest(dates) x
        WHERE  x >= sub.ts - interval '1h') AS ct
FROM (
   SELECT id, ts
        , array_agg(ts) OVER(ORDER BY ts) AS dates
   FROM   test
   ) sub;

COR - subconsulta correlacionada

podrías resolverlo con una subconsulta correlacionada simple. Mucho más rápido, pero aun así...

SELECT id, ts
     , (SELECT count(*)
        FROM   test t1
        WHERE  t1.ts >= t.ts - interval '1h'
        AND    t1.ts < t.ts) AS ct
FROM   test t
ORDER  BY ts;

FNC - Función

Recorra las filas en orden cronológico con un row_number() en función plpgsql y combínalo con un cursor sobre la misma consulta, abarcando el marco de tiempo deseado. Entonces podemos simplemente restar números de fila:

CREATE OR REPLACE FUNCTION running_window_ct(_intv interval = '1 hour')
  RETURNS TABLE (id bigint, ts timestamp, ct int)
  LANGUAGE plpgsql AS
$func$
DECLARE
   cur   CURSOR FOR
         SELECT t.ts + _intv AS ts1, row_number() OVER (ORDER BY t.ts) AS rn
         FROM   test t ORDER BY t.ts;
   rec   record;
   rn    int;

BEGIN
   OPEN cur;
   FETCH cur INTO rec;
   ct := -1;  -- init

   FOR id, ts, rn IN
      SELECT t.id, t.ts, row_number() OVER (ORDER BY t.ts)
      FROM   test t ORDER BY t.ts
   LOOP
      IF rec.ts1 >= ts THEN
         ct := ct + 1;
      ELSE
         LOOP
            FETCH cur INTO rec;
            EXIT WHEN rec.ts1 >= ts;
         END LOOP;
         ct := rn - rec.rn;
      END IF;

      RETURN NEXT;
   END LOOP;
END
$func$;

Llamada con intervalo predeterminado de una hora:

SELECT * FROM running_window_ct();

O con cualquier intervalo:

SELECT * FROM running_window_ct('2 hour - 3 second');

db<>violín aquí
Sqlfiddle antiguo

Valor de referencia

Con la tabla de arriba, ejecuté un punto de referencia rápido en mi antiguo servidor de prueba:(PostgreSQL 9.1.9 en Debian).

-- TRUNCATE test;
INSERT INTO test
SELECT g, '2013-08-08'::timestamp
         + g * interval '5 min'
         + random() * 300 * interval '1 min' -- halfway realistic values
FROM   generate_series(1, 10000) g;

CREATE INDEX test_ts_idx ON test (ts);
ANALYZE test;  -- temp table needs manual analyze

Varié las negritas parte para cada ejecución y tomó el mejor de 5 con EXPLAIN ANALYZE .

100 filas
ROM:27,656 ms
ARR:7,834 ms
COR:5,488 ms
FNC:1,115 ms

1000 filas
ROM:2116,029 ms
ARR:189,679 ms
COR:65,802 ms
FNC:8,466 ms

5000 filas
ROM:51347 ms !!
ARR:3167 ms
COR:333 ms
FNC:42 ms

100000 filas
ROM:DNF
ARR:DNF
COR:6760 ms
FNC:828 ms

La función es la clara vencedora. Es el más rápido por orden de magnitud y el que mejor escala.
El manejo de arreglos no puede competir.