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

Samlet antal poster pr. uge

Den enkle tilgang ville være at løse dette med en CROSS JOIN som demonstreret af @jpw. Der er dog nogle skjulte problemer :

  1. ydelsen af en ubetinget CROSS JOIN forringes hurtigt med stigende antal rækker. Det samlede antal rækker ganges med antallet af uger, du tester i, før denne enorme afledte tabel kan behandles i aggregeringen. Indekser kan ikke hjælpe.

  2. Start af uger med 1. januar fører til uoverensstemmelser. ISO-uger kan være et alternativ. Se nedenfor.

Alle de følgende forespørgsler gør stor brug af et indeks på exam_date . Sørg for at have en.

Tilslut kun til relevante rækker

Bør være meget hurtigere :

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;

Dette er med uger fra 1. januar som i din original. Som nævnt giver dette et par uoverensstemmelser:Uger starter på en anden dag hvert år, og da vi afskærer i slutningen af ​​året, består årets sidste uge af kun 1 eller 2 dage (skudår).

Det samme med ISO-uger

Overvej ISO-uger, afhængigt af kravene i stedet, som starter om mandagen og altid strækker sig over 7 dage. Men de krydser grænsen mellem år. Pr. dokumentation på EXTRACT() :

Ovenstående forespørgsel omskrevet med ISO-uger:

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;

Den 4. januar er altid i årets første ISO-uge. Så dette udtryk får datoen mandag i den første ISO-uge i det givne år:

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

Forenklet med EXTRACT()

Da ISO-uger falder sammen med ugenumrene returneret af EXTRACT() , kan vi forenkle forespørgslen. Først en kort og enkel formular:

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

Optimeret forespørgsel

Det samme med flere detaljer og optimeret til ydeevne

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

Dette skal være meget hurtigt med indeksunderstøttelse og kan nemt tilpasses til intervaller efter eget valg. Den implicitte JOIN LATERAL for generate_series() i den sidste forespørgsel kræver Postgres 9.3 .

SQL Fiddle.



  1. Enkel måde at nulstille Django PostgreSQL-databasen?

  2. Håndtering af nuller ved brug af Oracle XMLType

  3. mysql konverterer flere rækker til kolonner i en enkelt række

  4. Brug float eller decimal til regnskabsapplikation dollarbeløb?