PostgreSQL
 sql >> Datenbank >  >> RDS >> PostgreSQL

Gesamtzahl der Datensätze pro Woche

Der einfache Ansatz wäre, dies mit einem CROSS JOIN zu lösen, wie von @jpw demonstriert. Es gibt jedoch einige versteckte Probleme :

  1. 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.

  2. 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 .

SQL-Fiddle.