Общее количество записей в неделю

У меня есть база данных Postgres 9.1. Я пытаюсь создать количество записей в неделю (для заданного диапазона дат) и сравнить его с предыдущим годом.

У меня есть следующий код, используемый для создания серии:

select generate_series('2013-01-01', '2013-01-31', '7 day'::interval) as series

Однако я не уверен, как присоединить подсчитанные записи к сгенерированным датам.

Итак, на примере следующих записей:

Pt_ID      exam_date
======     =========
1          2012-01-02
2          2012-01-02
3          2012-01-08
4          2012-01-08
1          2013-01-02
2          2013-01-02
3          2013-01-03
4          2013-01-04
1          2013-01-08
2          2013-01-10
3          2013-01-15
4          2013-01-24

Я хотел, чтобы записи возвращались как:

  series        thisyr      lastyr
===========     =====       =====
2013-01-01        4           2
2013-01-08        3           2
2013-01-15        1           0
2013-01-22        1           0
2013-01-29        0           0

Не уверен, как ссылаться на диапазон дат в подпоиске. Спасибо за любую помощь.


person kipsoft    schedule 09.11.2014    source источник
comment
Как именно вы планировали провести неделю с 30.12.2012 по 05.01.2013? Недели пересекают границы между годами, поэтому ваша идея ошибочна: каждый год недели начинаются с другого дня, а последняя неделя более или менее усекается. Я предлагаю вместо этого использовать недели ISO: By definition (ISO 8601), weeks start on Mondays and the first week of a year contains January 4 of that year. Может ли это сработать для вас?   -  person Erwin Brandstetter    schedule 10.11.2014


Ответы (2)


Использование across joinshould работает, я просто вставлю выходные данные уценки из SQL Fiddle ниже. Может показаться, что ваш образец вывода неверен для серии 2013-01-08: thisyr должно быть 2, а не 3. Это может быть не лучший способ сделать это, хотя мои знания Postgresql оставляют желать лучшего.

скрипт SQL

Настройка схемы PostgreSQL 9.2.4:

CREATE TABLE Table1
    ("Pt_ID" varchar(6), "exam_date" date);

INSERT INTO Table1
    ("Pt_ID", "exam_date")
VALUES
    ('1', '2012-01-02'),('2', '2012-01-02'),
    ('3', '2012-01-08'),('4', '2012-01-08'),
    ('1', '2013-01-02'),('2', '2013-01-02'),
    ('3', '2013-01-03'),('4', '2013-01-04'),
    ('1', '2013-01-08'),('2', '2013-01-10'),
    ('3', '2013-01-15'),('4', '2013-01-24');

Запрос 1:

select 
  series, 
  sum (
    case 
      when exam_date 
        between series and series + '6 day'::interval
      then 1 
      else 0 
    end
  ) as thisyr,
  sum (
    case 
      when exam_date + '1 year'::interval 
        between series and series + '6 day'::interval
      then 1 else 0 
    end
  ) as lastyr

from table1
cross join generate_series('2013-01-01', '2013-01-31', '7 day'::interval) as series
group by series
order by series

Результаты:

|                         SERIES | THISYR | LASTYR |
|--------------------------------|--------|--------|
| January, 01 2013 00:00:00+0000 |      4 |      2 |
| January, 08 2013 00:00:00+0000 |      2 |      2 |
| January, 15 2013 00:00:00+0000 |      1 |      0 |
| January, 22 2013 00:00:00+0000 |      1 |      0 |
| January, 29 2013 00:00:00+0000 |      0 |      0 |
person jpw    schedule 09.11.2014
comment
Спасибо. Код работает хорошо. Я хотел бы кое-что понять, однако - когда сумма для ластыра, почему exam_date + '1 год' дает предыдущий год. Я немного смущен этим. - person kipsoft; 10.11.2014
comment
О, поскольку ряд относится к текущему году, а вы хотите подсчитать вхождения в предыдущем году, казалось, что проще всего просто добавить год к дате, в результате чего даты, прошедшие год назад, попадут в ряд. Альтернативой было бы сделать что-то вроде when exam_date between series - '1 year'::interval and series + '6 day -1 year'::intervalя думаю. Не уверен, как написать интервал в этом случае. - person jpw; 10.11.2014
comment
Спасибо. Я верю, что у меня это есть. Что бы вы предложили включить даты из другой таблицы (т.е. иметь еще два столбца с this_year и last_year из my_date в таблице 2). У меня небольшие проблемы с этим. - person kipsoft; 10.11.2014
comment
Не уверен, можно попробовать добавить еще одно перекрестное соединение и обращаться с ним как с первой таблицей? - person jpw; 10.11.2014
comment
@ErwinBrandstetter Это хороший момент, я думаю, это даст некоторые странные результаты, однако я думаю, что ваш комментарий должен быть адресован Kipsoft (не уверен, что ОП тоже получит уведомление о вашем комментарии); Я просто показал, как связать сгенерированный ряд с таблицей дат. В идеале я думаю, что использование таблицы календаря может быть лучшим выбором или что-то подобное. - person jpw; 10.11.2014
comment
@jpw: Извините, хотел обратиться к kipsoft, по ошибке написал под вашим ответом. Кстати, я согласен, что (series, thisyr) должно быть ('2013-01-08', 2), а не 3. Что касается запроса: перекрестное соединение работает хорошо, но производительность ухудшается при больших наборах. - person Erwin Brandstetter; 10.11.2014

Простым подходом было бы решить эту проблему с помощью CROSS JOIN, как показано @jpw. Однако есть некоторые скрытые проблемы:

  1. Производительность безусловного CROSS JOIN быстро ухудшается с ростом количества строк. Общее количество строк умножается на количество недель, в течение которых вы тестируете, прежде чем эта огромная производная таблица может быть обработана в агрегации. Индексы не могут помочь.

  2. Начало недели с 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 или 2 дней (високосный год).

То же самое с неделями ISO

В зависимости от требований вместо этого рассмотрите недели ISO, которые начинаются по понедельникам и всегда занимают 7 дней. Но они пересекают границу между годами. Согласно документации на EXTRACT():

неделя

Номер недели года, в котором находится день. По определению (ISO 8601) недели начинаются с понедельника, а первая неделя года содержит 4 января этого года. Другими словами, первый четверг года приходится на неделю 1 этого года.

В определении ISO даты начала января могут быть частью 52-й или 53-й недели предыдущего года, а даты конца декабря - частью первой недели следующего года. Например, 2005-01-01 является частью 53-й недели 2004 года, а 2006-01-01 — частью 52-й недели 2005 года, а 2012-12-31 — частью первой недели 2013 года. Для согласованности рекомендуется использовать поле isoyear вместе с week. полученные результаты.

Приведенный выше запрос переписан с использованием недель 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;

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.

скрипт SQL.

person Erwin Brandstetter    schedule 10.11.2014