PostgreSQL
 sql >> Base de données >  >> RDS >> PostgreSQL

Nombre total d'enregistrements par semaine

L'approche simple serait de résoudre ce problème avec un CROSS JOIN comme démontré par @jpw. Cependant, il existe des problèmes cachés :

  1. Les performances d'un CROSS JOIN inconditionnel se détériore rapidement avec l'augmentation du nombre de rangées. Le nombre total de lignes est multiplié par le nombre de semaines pendant lesquelles vous testez, avant que cette énorme table dérivée puisse être traitée dans l'agrégation. Les index ne peuvent pas aider.

  2. Commencer les semaines avec le 1er janvier entraîne des incohérences. Semaines ISO peut être une alternative. Voir ci-dessous.

Toutes les requêtes suivantes utilisent intensivement un index le exam_date . Assurez-vous d'en avoir un.

Joindre uniquement aux lignes pertinentes

Ça devrait être beaucoup plus rapide :

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;

C'est avec des semaines à partir du 1er janvier comme dans votre original. Comme indiqué, cela produit quelques incohérences :les semaines commencent un jour différent chaque année et puisque nous coupons à la fin de l'année, la dernière semaine de l'année se compose de seulement 1 ou 2 jours (année bissextile).

Pareil pour les semaines ISO

En fonction des besoins, envisagez des semaines ISO au lieu de cela, qui commencent le lundi et s'étendent toujours sur 7 jours. Mais ils traversent la frontière entre les années. Par documentation sur EXTRACT() :

Requête ci-dessus réécrite avec des semaines 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;

Le 4 janvier correspond toujours à la première semaine ISO de l'année. Cette expression obtient donc la date du lundi de la première semaine ISO de l'année donnée :

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

Simplifier avec EXTRACT()

Étant donné que les semaines ISO coïncident avec les numéros de semaine renvoyés par EXTRACT() , nous pouvons simplifier la requête. Tout d'abord, un formulaire court et simple :

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

Requête optimisée

Le même avec plus de détails et optimisé pour les performances

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

Cela devrait être très rapide avec le support d'index et peut facilement être adapté aux intervalles de choix. L'implicite JOIN LATERAL pour generate_series() dans la dernière requête nécessite Postgres 9.3 .

SQL Fiddle.