L'approccio semplice sarebbe quello di risolverlo con un CROSS JOIN come dimostrato da @jpw. Tuttavia, ci sono alcuni problemi nascosti :
-
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. -
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 .