PostgreSQL
 sql >> Database >  >> RDS >> PostgreSQL

Numero totale di record a settimana

L'approccio semplice sarebbe quello di risolverlo con un CROSS JOIN come dimostrato da @jpw. Tuttavia, ci sono alcuni problemi nascosti :

  1. Il rendimento di un CROSS JOIN incondizionato si deteriora rapidamente con l'aumentare del numero di file. Il numero totale di righe viene moltiplicato per il numero di settimane per cui stai testando, prima che questa enorme tabella derivata possa essere elaborata nell'aggregazione. Gli indici non possono aiutare.

  2. Iniziare settimane con il 1° gennaio porta a incongruenze. Settimane ISO potrebbe essere un'alternativa. Vedi sotto.

Tutte le seguenti query fanno un uso massiccio di un indice su exam_date . Assicurati di averne uno.

Unisciti solo alle righe pertinenti

Dovrebbe essere molto più veloce :

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;

Questo è con settimane a partire dal 1 gennaio come nel tuo originale. Come commentato, questo produce un paio di incongruenze:le settimane iniziano in un giorno diverso ogni anno e poiché si interrompe alla fine dell'anno, l'ultima settimana dell'anno è composta da solo 1 o 2 giorni (anno bisestile).

Lo stesso con le settimane ISO

A seconda dei requisiti, considera Settimane ISO invece, che iniziano il lunedì e durano sempre 7 giorni. Ma attraversano il confine tra gli anni. Per documentazione su EXTRACT() :

Query sopra riscritta con settimane 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;

Il 4 gennaio è sempre la prima settimana ISO dell'anno. Quindi questa espressione ottiene la data del lunedì della prima settimana ISO dell'anno specificato:

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

Semplifica con EXTRACT()

Poiché le settimane ISO coincidono con i numeri delle settimane restituiti da EXTRACT() , possiamo semplificare la query. Innanzitutto una forma breve e semplice:

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);

Richiesta ottimizzata

Lo stesso con maggiori dettagli e ottimizzato per le prestazioni

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);

Questo dovrebbe essere molto veloce con il supporto dell'indice e può essere facilmente adattato a intervalli di scelta. L'implicito JOIN LATERAL per generate_series() nell'ultima query richiede Postgres 9.3 .

SQL Fiddle.