Агрегирование списка дат начала и окончания - PullRequest
2 голосов
/ 08 июня 2010

У меня есть список дат и идентификаторов, и я хотел бы свернуть их в периоды фактических дат внутри каждого идентификатора.

Для таблицы со столбцами «testid» и «pulldate» втаблица с именем "data":

| A79 | 2010-06-02 |
| A79 | 2010-06-03 |
| A79 | 2010-06-04 |
| B72 | 2010-04-22 |
| B72 | 2010-06-03 |
| B72 | 2010-06-04 |
| C94 | 2010-04-09 |
| C94 | 2010-04-10 |
| C94 | 2010-04-11 |
| C94 | 2010-04-12 |
| C94 | 2010-04-13 |
| C94 | 2010-04-14 |
| C94 | 2010-06-02 |
| C94 | 2010-06-03 |
| C94 | 2010-06-04 |

Я хочу создать таблицу со столбцами "testid", "group", "start_date", "end_date":

| A79 | 1 | 2010-06-02 | 2010-06-04 |
| B72 | 2 | 2010-04-22 | 2010-04-22 |
| B72 | 3 | 2010-06-03 | 2010-06-04 |
| C94 | 4 | 2010-04-09 | 2010-04-14 |
| C94 | 5 | 2010-06-02 | 2010-06-04 |

Вот код, который я придумал:

SELECT t2.testid,
  t2.group,
  MIN(t2.pulldate) AS start_date,
  MAX(t2.pulldate) AS end_date
FROM(SELECT t1.pulldate,
  t1.testid,
  SUM(t1.check) OVER (ORDER BY t1.testid,t1.pulldate) AS group
FROM(SELECT data.pulldate,
  data.testid,
  CASE
  WHEN data.testid=LAG(data.testid,1) 
    OVER (ORDER BY data.testid,data.pulldate)
  AND data.pulldate=date (LAG(data.pulldate,1) 
    OVER (PARTITION BY data.testid 
    ORDER BY data.pulldate)) + integer '1'
  THEN 0
  ELSE 1
  END AS check
FROM data 
ORDER BY data.testid, data.pulldate) AS t1) AS t2
GROUP BY t2.testid,t2.group
ORDER BY t2.group;

Я использовал оконную функцию LAG для сравнения каждой строки с предыдущей, поставив 1, если мне нужно увеличить значение, чтобы начать новую группу, затем я делаютекущую сумму в этом столбце, а затем объединить в комбинации «группа» и «тестид».

Есть ли лучший способ для достижения моей цели, или эта операция имеет имя?

Я использую PostgreSQL 8.4

Ответы [ 2 ]

1 голос
/ 08 июня 2010

Вот еще один подход:

WITH TEMP_TAB AS (
SELECT testid, pulldate,
       (pulldate + (row_number || ' days')::interval)::date AS dummydate
 FROM ( SELECT *, row_number() OVER () FROM
    ( SELECT * FROM data ORDER BY testid,pulldate DESC
    ) AS tab1 
 ) AS tab2 
)
SELECT * FROM (
  SELECT testid, min(pulldate) AS mindate, max(pulldate) AS maxdate 
    FROM TEMP_TAB GROUP BY testid,dummydate 
  )  AS tab3 
ORDER BY testid, mindate

Предупреждение: эта стратегия ломается, если есть повторяющиеся пары (testid, pulldate). В этом случае сначала нужно сделать DISTINCT над этими полями.

Объяснение : в промежуточной таблице есть dummydate, полученное путем добавления количества дней, равного «номеру строки» (в упорядоченном выборе); его единственное значение состоит в том, что строки с одинаковыми dummydate находятся в одном и том же наборе последовательных дат. Например: промежуточные результаты:

test=#  SELECT *, row_number() OVER  () FROM
test-#   ( SELECT * FROM data ORDER BY testid,pulldate DESC) AS tab1;
 testid |  pulldate  | row_number
--------+------------+------------
 A79    | 2010-06-04 |          1
 A79    | 2010-06-03 |          2
 A79    | 2010-06-02 |          3
 B72    | 2010-06-04 |          4
 B72    | 2010-06-03 |          5
 B72    | 2010-04-22 |          6
 C94    | 2010-06-04 |          7
 C94    | 2010-06-03 |          8
 C94    | 2010-06-02 |          9
 C94    | 2010-04-14 |         10
 C94    | 2010-04-13 |         11
 C94    | 2010-04-12 |         12
 C94    | 2010-04-11 |         13
 C94    | 2010-04-10 |         14
 C94    | 2010-04-09 |         15



test=# SELECT
test-#  testid,pulldate,(pulldate + (row_number || 'days')::interval)::date AS dummydate
test-#  FROM ( SELECT *, row_number() OVER  () FROM
test(#   ( SELECT * FROM data ORDER BY testid,pulldate DESC) AS tab1 )
test-#  AS tab2;
 testid |  pulldate  | dummydate
--------+------------+------------
 A79    | 2010-06-04 | 2010-06-05
 A79    | 2010-06-03 | 2010-06-05
 A79    | 2010-06-02 | 2010-06-05
 B72    | 2010-06-04 | 2010-06-08
 B72    | 2010-06-03 | 2010-06-08
 B72    | 2010-04-22 | 2010-04-28
 C94    | 2010-06-04 | 2010-06-11
 C94    | 2010-06-03 | 2010-06-11
 C94    | 2010-06-02 | 2010-06-11
 C94    | 2010-04-14 | 2010-04-24
 C94    | 2010-04-13 | 2010-04-24
 C94    | 2010-04-12 | 2010-04-24
 C94    | 2010-04-11 | 2010-04-24
 C94    | 2010-04-10 | 2010-04-24
 C94    | 2010-04-09 | 2010-04-24

Редактировать: СО здесь не нужен (но, тем не менее, он мне нравится), это то же самое:

SELECT * FROM (
  SELECT testid, min(pulldate) AS mindate, max(pulldate) AS maxdate 
  FROM (
    SELECT
      testid,pulldate,
      (pulldate + (row_number || ' days')::interval)::date AS dummydate
    FROM ( SELECT *, row_number() OVER  () FROM
      ( 
       SELECT * FROM data ORDER BY testid,pulldate DESC) AS tab1 )  
       AS tab2 
    ) as temp_tab
  GROUP BY testid,dummydate 
)  AS tab3
ORDER BY testid, mindate
1 голос
/ 08 июня 2010

Я не знаю ни одного известного названия для этой техники. Я попытался написать это сам и придумал что-то, по сути, эквивалентное вашему, отличающееся только наличием на один WindowAgg меньше.

select testid, group_num as group,
       min(pulldate) as start_date,
       max(pulldate) as end_date
from (select testid,
             pulldate,
             sum(case when projected_pulldate is null or pulldate <> projected_pulldate
                      then 1 else 0 end) over (order by testid, pulldate) as group_num
      from (select testid, pulldate,
                   (lag(pulldate, 1) over (partition by testid order by pulldate)
                   ) + 1 as projected_pulldate
            from data) x
     ) grouped
group by testid, group_num
order by 1, 2

Это вряд ли красиво, и мне интересно, если это просто тот случай, когда лучше использовать plpgsql или подобное.

create or replace function data_extents()
 returns table(testid char(3), "group" int, start_date date, end_date date)
 language plpgsql
 stable as $$
declare
  rec data%rowtype;
begin
  "group" := 1;
  for rec in select * from data order by testid, pulldate loop
    if testid is null then
      -- first row
      testid := rec.testid;
      start_date := rec.pulldate;
      end_date := rec.pulldate;
    elsif rec.testid <> testid or rec.pulldate <> (end_date + 1) then
      -- discontinuity
      return next;
      testid := rec.testid;
      start_date := rec.pulldate;
      end_date := rec.pulldate;
      "group" := "group" + 1;
    else
      end_date := end_date + 1;
    end if;
  end loop;
  if testid is not null then
    return next;
  end if;
end;
$$;

Это тоже вряд ли симпатично ... хотя в принципе он выводит результаты одного сканирования без нескольких разных агрегаций, что по крайней мере кажется лучше. Это занимает столько же времени для крошечного набора данных; больший набор данных? Я еще не пробовал это.

Поскольку ни одно из наших решений не позволяет использовать предикаты, такие как "testid = XXX", для сканирования данных (afaict), функция может быть единственным способом эффективной фильтрации?

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...