簡単なアプローチは、@jpwで示されているようなCROSSJOINを使用してこれを解決することです。ただし、いくつかの隠れた問題があります :
-
パフォーマンス 無条件の
CROSS JOIN
の 行数が増えるとすぐに劣化します。この巨大な派生テーブルを集計で処理する前に、行の総数にテストする週数を掛けます。インデックスは役に立ちません。 -
1月1日から数週間を開始すると、不整合が発生します。 ISO週間 代替案かもしれません。以下を参照してください。
以下のクエリはすべて、exam_date
のインデックスを多用します。 。必ず持ってください。
関連する行にのみ結合する
はるかに高速である必要があります :
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;
これはあなたのオリジナルのように1月1日から始まる週です。コメントされているように、これにはいくつかの矛盾が生じます。週は毎年異なる日に始まり、年末に終了するため、年の最後の週は1日または2日(うるう年)で構成されます。
ISO週と同じ
要件に応じて、ISO週間を検討してください 代わりに、月曜日に開始し、常に7日間に渡ります。しかし、彼らは年の間に国境を越えます。 EXTRACT()
:
上記のクエリは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;
1月4日は常にその年の最初のISO週です。したがって、この式は、指定された年の最初のISO週の月曜日の日付を取得します。
date_trunc('week', '2012-01-04'::date)::date
EXTRACT()
ISOの週は、EXTRACT()
によって返される週番号と一致するため 、クエリを簡略化できます。まず、短くて単純な形式:
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);
最適化されたクエリ
詳細についても同じで、パフォーマンスが最適化されています
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);
これはインデックスのサポートにより非常に高速であり、選択した間隔に簡単に適合させることができます。暗黙のJOIN LATERAL
generate_series()
の場合 最後のクエリでは、 Postgres 9.3が必要です 。