PostgreSQL
 sql >> Base de Dados >  >> RDS >> PostgreSQL

Número total de registros por semana


A abordagem simples seria resolver isso com um CROSS JOIN como demonstrado por @jpw. No entanto, existem alguns problemas ocultos :

  1. O desempenho de um CROSS JOIN incondicional deteriora-se rapidamente com o número crescente de linhas. O número total de linhas é multiplicado pelo número de semanas que você está testando, antes que essa enorme tabela derivada possa ser processada na agregação. Os índices não podem ajudar.

  2. Começar semanas com 1º de janeiro leva a inconsistências. semanas ISO pode ser uma alternativa. Veja abaixo.

Todas as consultas a seguir fazem uso intenso de um índice em exam_date . Certifique-se de ter um.

Junte-se apenas a linhas relevantes


Deve ser muito mais 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;

Isso com semanas a partir de 1º de janeiro, como no original. Como comentado, isso produz algumas inconsistências:As semanas começam em um dia diferente a cada ano e, como o corte é no final do ano, a última semana do ano consiste em apenas 1 ou 2 dias (ano bissexto).

O mesmo com semanas ISO


Dependendo dos requisitos, considere semanas ISO em vez disso, que começam às segundas-feiras e sempre abrangem 7 dias. Mas eles cruzam a fronteira entre os anos. Por documentação em EXTRACT() :

Consulta acima reescrita com 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;

4 de janeiro é sempre na primeira semana ISO do ano. Portanto, esta expressão obtém a data de segunda-feira da primeira semana ISO do ano fornecido:
date_trunc('week', '2012-01-04'::date)::date

Simplifique com EXTRACT()


Como as semanas ISO coincidem com os números das semanas retornados por EXTRACT() , podemos simplificar a consulta. Primeiro, um formulário curto e simples:
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 otimizada


O mesmo com mais detalhes e otimizado para desempenho
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);

Isso deve ser muito rápido com suporte a índice e pode ser facilmente adaptado aos intervalos de escolha. O JOIN LATERAL implícito para generate_series() na última consulta requer Postgres 9.3 .

SQL Fiddle.