2010-06-08 5 views
2

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

Для таблицы с колонками «TestID» и «pulldate» в таблице под названием «Данные»:

| 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», «группа», " 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, если мне нужно увеличиться, чтобы начать новую группу, я затем выполняю текущую сумму этого столбца, а затем объединяю их в комбинации «group» и «testid».

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

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

ответ

1

Вот другой подход:

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) пары. В этом случае сначала нужно сделать ОТКЛЮЧЕНИЕ по этим полям.

Объяснение: Промежуточная таблица имеет 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 

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

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

Я не знаю ни одного известного имени для этой техники. Я попытался написать его сам и придумал что-то по существу эквивалентное вашему, отличающееся только тем, что у него меньше 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; 
$$; 

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

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