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

Número total de registros por semana

El enfoque simple sería resolver esto con CROSS JOIN como lo demuestra @jpw. Sin embargo, hay algunos problemas ocultos :

  1. El rendimiento de un CROSS JOIN incondicional se deteriora rápidamente con el número creciente de filas. El número total de filas se multiplica por el número de semanas que está probando, antes de que esta enorme tabla derivada pueda procesarse en la agregación. Los índices no pueden ayudar.

  2. Comenzar semanas con el 1 de enero genera inconsistencias. Semanas ISO podría ser una alternativa. Ver más abajo.

Todas las siguientes consultas hacen un uso intensivo de un índice en exam_date . Asegúrate de tener uno.

Únete solo a filas relevantes

Debería ser mucho más rápido :

SELECT d.day, d.thisyr
     , count(t.exam_date) AS lastyr
FROM  (
   SELECT d.day::date, (d.day - '1 year'::interval)::date AS day0  -- for 2nd join
        , count(t.exam_date) AS thisyr
   FROM   generate_series('2013-01-01'::date
                        , '2013-01-31'::date  -- last week overlaps with Feb.
                        , '7 days'::interval) d(day)  -- returns timestamp
   LEFT   JOIN tbl t ON t.exam_date >= d.day::date
                    AND t.exam_date <  d.day::date + 7
   GROUP  BY d.day
   ) d
LEFT   JOIN tbl t ON t.exam_date >= d.day0      -- repeat with last year
                 AND t.exam_date <  d.day0 + 7
GROUP  BY d.day, d.thisyr
ORDER  BY d.day;

Esto es con semanas a partir del 1 de enero como en su original. Como comentamos, esto produce un par de inconsistencias:Las semanas comienzan en un día diferente cada año y como cortamos al final del año, la última semana del año consta de solo 1 o 2 días (año bisiesto).

Lo mismo con las semanas ISO

Según los requisitos, considere semanas ISO en cambio, que comienzan los lunes y siempre duran 7 días. Pero cruzan la frontera entre años. Según documentación en EXTRACT() :

Consulta anterior reescrita con semanas ISO:

SELECT w AS isoweek
     , day::text  AS thisyr_monday, thisyr_ct
     , day0::text AS lastyr_monday, count(t.exam_date) AS lastyr_ct
FROM  (
   SELECT w, day
        , date_trunc('week', '2012-01-04'::date)::date + 7 * w AS day0
        , count(t.exam_date) AS thisyr_ct
   FROM  (
      SELECT w
           , date_trunc('week', '2013-01-04'::date)::date + 7 * w AS day
      FROM   generate_series(0, 4) w
      ) d
   LEFT   JOIN tbl t ON t.exam_date >= d.day
                    AND t.exam_date <  d.day + 7
   GROUP  BY d.w, d.day
   ) d
LEFT   JOIN tbl t ON t.exam_date >= d.day0     -- repeat with last year
                 AND t.exam_date <  d.day0 + 7
GROUP  BY d.w, d.day, d.day0, d.thisyr_ct
ORDER  BY d.w, d.day;

El 4 de enero siempre se encuentra en la primera semana ISO del año. Entonces esta expresión obtiene la fecha del lunes de la primera semana ISO del año dado:

date_trunc('week', '2012-01-04'::date)::date

Simplificar con EXTRACT()

Dado que las semanas ISO coinciden con los números de semana devueltos por EXTRACT() , podemos simplificar la consulta. Primero, un formulario corto y simple:

SELECT w AS isoweek
     , COALESCE(thisyr_ct, 0) AS thisyr_ct
     , COALESCE(lastyr_ct, 0) AS lastyr_ct
FROM   generate_series(1, 5) w
LEFT   JOIN (
   SELECT EXTRACT(week FROM exam_date)::int AS w, count(*) AS thisyr_ct
   FROM   tbl
   WHERE  EXTRACT(isoyear FROM exam_date)::int = 2013
   GROUP  BY 1
   ) t13  USING (w)
LEFT   JOIN (
   SELECT EXTRACT(week FROM exam_date)::int AS w, count(*) AS lastyr_ct
   FROM   tbl
   WHERE  EXTRACT(isoyear FROM exam_date)::int = 2012
   GROUP  BY 1
   ) t12  USING (w);

Consulta optimizada

Lo mismo con más detalles y optimizado para el rendimiento

WITH params AS (          -- enter parameters here, once 
   SELECT date_trunc('week', '2012-01-04'::date)::date AS last_start
        , date_trunc('week', '2013-01-04'::date)::date AS this_start
        , date_trunc('week', '2014-01-04'::date)::date AS next_start
        , 1 AS week_1
        , 5 AS week_n     -- show weeks 1 - 5
   )
SELECT w.w AS isoweek
     , p.this_start + 7 * (w - 1) AS thisyr_monday
     , COALESCE(t13.ct, 0) AS thisyr_ct
     , p.last_start + 7 * (w - 1) AS lastyr_monday
     , COALESCE(t12.ct, 0) AS lastyr_ct
FROM params p
   , generate_series(p.week_1, p.week_n) w(w)
LEFT   JOIN (
   SELECT EXTRACT(week FROM t.exam_date)::int AS w, count(*) AS ct
   FROM   tbl t, params p
   WHERE  t.exam_date >= p.this_start      -- only relevant dates
   AND    t.exam_date <  p.this_start + 7 * (p.week_n - p.week_1 + 1)::int
-- AND    t.exam_date <  p.next_start      -- don't cross over into next year
   GROUP  BY 1
   ) t13  USING (w)
LEFT   JOIN (                              -- same for last year
   SELECT EXTRACT(week FROM t.exam_date)::int AS w, count(*) AS ct
   FROM   tbl t, params p
   WHERE  t.exam_date >= p.last_start
   AND    t.exam_date <  p.last_start + 7 * (p.week_n - p.week_1 + 1)::int
-- AND    t.exam_date <  p.this_start
   GROUP  BY 1
   ) t12  USING (w);

Esto debería ser muy rápido con soporte de índice y se puede adaptar fácilmente a los intervalos de elección. El JOIN LATERAL implícito para generate_series() en la última consulta requiere Postgres 9.3 .

SQL Fiddle.