Der einfache Ansatz wäre, dies mit einem CROSS JOIN zu lösen, wie von @jpw demonstriert. Es gibt jedoch einige versteckte Probleme :
-
Die Leistung eines unbedingten
CROSS JOIN
verschlechtert sich schnell mit wachsender Anzahl von Reihen. Die Gesamtzahl der Zeilen wird mit der Anzahl der Wochen multipliziert, für die Sie testen, bevor diese riesige abgeleitete Tabelle in der Aggregation verarbeitet werden kann. Indizes können nicht helfen. -
Das Beginnen von Wochen mit dem 1. Januar führt zu Ungereimtheiten. ISO-Wochen wäre vielleicht eine Alternative. Siehe unten.
Alle folgenden Abfragen machen intensiven Gebrauch von einem Index am exam_date
. Stellen Sie sicher, dass Sie eine haben.
Nur relevanten Zeilen beitreten
Sollte viel schneller sein :
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;
Dies ist mit Wochen ab dem 1. Januar wie in Ihrem Original. Wie bereits erwähnt, führt dies zu einigen Ungereimtheiten:Die Wochen beginnen jedes Jahr an einem anderen Tag, und da wir am Ende des Jahres abschneiden, besteht die letzte Woche des Jahres nur aus 1 oder 2 Tagen (Schaltjahr).
Dasselbe gilt für ISO-Wochen
Berücksichtigen Sie je nach Bedarf ISO-Wochen stattdessen, die montags beginnen und sich immer über 7 Tage erstrecken. Aber sie überschreiten die Grenze zwischen den Jahren. Pro Dokumentation zu EXTRACT()
:
Obige Abfrage umgeschrieben mit ISO-Wochen:
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;
Der 4. Januar liegt immer in der ersten ISO-Woche des Jahres. Dieser Ausdruck erhält also das Datum des Montags der ersten ISO-Woche des angegebenen Jahres:
date_trunc('week', '2012-01-04'::date)::date
Vereinfachen mit EXTRACT()
Da ISO-Wochen mit den von EXTRACT()
zurückgegebenen Wochennummern übereinstimmen , können wir die Abfrage vereinfachen. Zuerst eine kurze und einfache Form:
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);
Optimierte Abfrage
Dasselbe mit mehr Details und optimiert für Leistung
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);
Dies sollte mit Indexunterstützung sehr schnell sein und kann leicht an beliebige Intervalle angepasst werden. Der implizite JOIN LATERAL
für generate_series()
in der letzten Abfrage erfordert Postgres 9.3 .