объединить DATE-строки, если эпизоды находятся в прямой последовательности или перекрываются - PullRequest
3 голосов
/ 30 мая 2011

У меня есть такая таблица:

ID    BEGIN    END

Если есть перекрывающиеся эпизоды для одного и того же идентификатора (например, 2000-01-01 - 2001-12-31 и 2000-06-01 - 2002-06-31), я бы хотелстроки, которые должны быть объединены, используя MIN(BEGIN), MAX(END).

То же самое должно быть сделано, если эпизоды находятся в прямой последовательности (как 2000-01-01 - 2000-06-31 и 2000-07-01 - 2000-12-31).

Если между эпизодами есть «пропущенные» дни (например, 2000-01-01 - 2000-06-15 и 2000-07-01 - 2000-12-31), они должны не объединяться.

Как этого достичь?

В настоящее время мой код выглядит следующим образом:

SELECT "ID", MIN("BEGIN"), MAX("END")
FROM ...
GROUP BY "ID"

, но, конечно, это не соответствует последнему условию (не объединять, если есть«пропущенные» дни).

Заранее спасибо!

[править]

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

SELECT "ID", LEAST(tab1."BEGIN", tab2."BEGIN"), GREATEST(tab1."END", tab2."END")
  FROM <mytable> AS tab1
  JOIN <mytable> AS tab2
    ON tab1."ID" = tab2."ID"
    AND  (tab1."BEGIN", tab1."END" + INTERVAL '2 day') OVERLAPS (tab2."BEGIN", tab2."END")
  ORDER BY "ID"

[править 2]

Спасибо за вашу помощь!

Я пытался выяснить, как оконные функции и запросы WITH работают уже несколько часов - пока я не понял, что моя база данных работает на PostGreSQL 8.3 (которая не поддерживает ни одну из них).Есть ли способ обойтись без оконных функций и WITH-запросов?

Еще раз спасибо!

[edit 3]

Пример данных:

ID        BEGIN         END
1;"2000-01-01";"2000-03-31"  
1;"2000-04-01";"2000-05-31"  
1;"2000-04-15";"2000-07-31"  
1;"2000-09-01";"2000-10-31"  
2;"2000-02-01";"2000-03-15"  
2;"2000-01-15";"2000-03-31"  
2;"2000-04-01";"2000-04-15"  
3;"2000-06-01";"2000-06-15"  
3;"2000-07-01";"2000-07-15"  

Пример вывода:

ID        BEGIN         END
1;"2000-01-01";"2000-07-31"
1;"2000-09-01";"2000-10-31"
2;"2000-01-15";"2000-04-15"
3;"2000-06-01";"2000-06-15"
3;"2000-07-01";"2000-07-15"

[править 4]

одно из возможных решений:

WITH
  t1 AS (
    SELECT id, begin AS time
      FROM "nace-8510-test".checkfkt
    UNION ALL
    SELECT id, end
      FROM "nace-8510-test".checkfkt
  ),

  t2 AS (
    SELECT Row_Number() OVER(PARTITION BY id ORDER BY time) AS num, id, time
      FROM t1 AS t1_1
  ),

  t3 AS (
    SELECT t2_1.num - Row_Number() OVER(PARTITION BY t2_1.id ORDER BY t2_1.time, t2_2.time) num1,
        t2_1.id, t2_1.time AS begin, t2_2.time AS end
      FROM t2 AS t2_1
        INNER JOIN t2 AS t2_2
          ON t2_1.id = t2_2.id
            AND t2_1.num = t2_2.num - 1
      WHERE
        EXISTS (
          SELECT *
            FROM "nace-8510-test".checkfkt AS s
            WHERE s.id = t2_1.id
              AND (s.begin < t2_2.time AND s.end > t2_1.time)
        )
        OR t2_1.time = t2_2.time
        OR t2_1.time + INTERVAL '1 day' = t2_2.time
  )

SELECT id, MIN(begin) AS von, MAX(end) AS bis
  FROM t3
  GROUP BY id, num1
  ORDER BY id

Большое спасибо автору этой статьи: http://blog.developpez.com/sqlpro/p9821/langage-sql-norme/agregation-d-intervalles-en-sql-1/

Ответы [ 4 ]

3 голосов
/ 30 мая 2011

Edit: Это отличная новость, что ваш администратор базы данных согласился перейти на более новую версию PostgreSQL.Оконные функции сами по себе делают обновление выгодным вложением.

Мой первоначальный ответ - как вы заметили - имеет существенный недостаток: ограничение в одну строку на id.
Ниже приведено лучшее решение без такого ограничения.
Я проверил его, используятестовые таблицы в моей системе (8.4).

Если / когда вы получите момент, я хотел бы знать, как он работает с вашими данными.
Я также написал объяснение здесь: http://adam -bernier.appspot.com / post/ 91001 / recursive-sql-example

WITH RECURSIVE t1_rec ( id, "begin", "end", n ) AS (
    SELECT id, "begin", "end", n
      FROM (
        SELECT
            id, "begin", "end",
            CASE 
                WHEN LEAD("begin") OVER (
                PARTITION BY    id
                ORDER BY        "begin") <= ("end" + interval '2' day) 
                THEN 1 ELSE 0 END AS cl,
            ROW_NUMBER() OVER (
                PARTITION BY    id
                ORDER BY        "begin") AS n
        FROM mytable 
    ) s
    WHERE s.cl = 1
  UNION ALL
    SELECT p1.id, p1."begin", p1."end", a.n
      FROM t1_rec a 
           JOIN mytable p1 ON p1.id = a.id
       AND p1."begin" > a."begin"
       AND (a."begin",  a."end" + interval '2' day) OVERLAPS 
           (p1."begin", p1."end")
)
SELECT t1.id, min(t1."begin"), max(t1."end")
  FROM t1_rec t1
       LEFT JOIN t1_rec t2 ON t1.id = t2.id 
       AND t2."end" = t1."end"
       AND t2.n < t1.n
 WHERE t2.n IS NULL
 GROUP BY t1.id, t1.n
 ORDER BY t1.id, t1.n;

Ниже следует оригинальный (устарелый) ответ;
примечание: ограничение по одной строке на id.


Денис, вероятно, прав насчет использования lead() и lag(), но есть и другой способ!
Вы также можете решить эту проблему с помощью так называемого рекурсивного SQL.
Функция перекрытия также пригодится.

Я полностью протестировал это решение на моей системе (8.4).
Хорошо работает.

WITH RECURSIVE rec_stmt ( id, begin, end ) AS (
    /* seed statement: 
           start with only first start and end dates for each id 
    */
      SELECT id, MIN(begin), MIN(end)
        FROM mytable seed_stmt
    GROUP BY id

    UNION ALL

    /* iterative (not really recursive) statement: 
           append qualifying rows to resultset 
    */
      SELECT t1.id, t1.begin, t1.end
        FROM rec_stmt r
             JOIN mytable t1 ON t1.id = r.id
         AND t1.begin > r.end
         AND (r.begin, r.end + INTERVAL '1' DAY) OVERLAPS 
             (t1.begin - INTERVAL '1' DAY, t1.end)
)
  SELECT MIN(begin), MAX(end) 
    FROM rec_stmt
GROUP BY id;
2 голосов
/ 30 мая 2011

Я не совсем понимаю ваш вопрос, но я абсолютно уверен, что вам нужно взглянуть на lead()/lag() оконные функции .

Например, что-то вроде этого будет хорошей отправной точкой для размещения в подзапросе или в общем табличном выражении , чтобы определить, перекрываются или нет строки для каждого идентификатора:

select id,
       lag(start) over w as prev_start,
       lag(end) over w as prev_end,
       start,
       end,
       lead(start) over w as next_start,
       lead(end) over w as next_end
from yourtable
window w as (
       partition by id
       order by start, end
       )
0 голосов
/ 15 июня 2011

Чистый SQL

Для чистого SQL-решения, посмотрите на пост Адама и прочитайте эту статью эту статью (она написана на французском языке, однако вы обнаружите, что ее не слишком сложно читать ). Эта статья была рекомендована мне после ознакомления со списком рассылки postgresql (спасибо за это!).

Для моих данных это не подходило, потому что все возможные решения должны самостоятельно присоединяться к таблице как минимум 3 раза. Это оказывается проблемой для (очень) больших объемов данных.

Полу SQL, Полуимперативный язык

Если вас в первую очередь волнует скорость и у вас есть возможность использовать императивный язык, вы можете получить намного быстрее (конечно, в зависимости от объема данных). В моем случае задача выполнялась (как минимум) в 1.000 раз быстрее, используя R.

Шаги:

(1) Получить .csv-файл. Позаботьтесь о сортировке !!!

COPY (
  SELECT "ID", "BEGIN", "END"
  <sorry, for a reason I don't know StackOverflow won't let me finish my code here...>

(2) Сделайте что-то подобное (этот код - R, но вы можете сделать что-то подобное на любом императивном языке):

data - read.csv2("</path/to.csv>")
data$BEGIN - as.Date(data$BEGIN)
data$END - as.Date(data$END)

smoothingEpisodes - function (theData) {

    theLength - nrow(theData)
    if (theLength  2L) return(theData)

    ID - as.integer(theData[["ID"]])
    BEGIN - as.numeric(theData[["BEGIN"]])
    END - as.numeric(theData[["END"]])

    curId - ID[[1L]]
    curBEGIN - BEGIN[[1L]]
    curEND - END[[1L]]



    out.1 - integer(length = theLength)
    out.2 - out.3 - numeric(length = theLength)

    j - 1L

    for(i in 2:nrow(theData)) {
        nextId - ID[[i]]
        nextBEGIN - BEGIN[[i]]
        nextEND - END[[i]]

        if (curId != nextId | (curEND + 1)  nextBEGIN) {
            out.1[[j]] - curId
            out.2[[j]] - curBEGIN
            out.3[[j]] - curEND

            j - j + 1L

            curId - nextId
            curBEGIN - nextBEGIN
            curEND - nextEND
        } else {
            curEND - max(curEND, nextEND, na.rm = TRUE)
        }
    }

    out.1[[j]] - curId
    out.2[[j]] - curBEGIN
    out.3[[j]] - curEND

    theOutput - data.frame(ID = out.1[1:j], BEGIN = as.Date(out.2[1:j], origin = "1970-01-01"), END = as.Date(out.3[1:j], origin = "1970-01-01"))

    theOutput
}

data1 - smoothingEpisodes(data)

data2 - transform(data1, TAGE = (as.numeric(data1$END - data1$BEGIN) + 1))

write.csv2(data2, file = "</path/to/output.csv>")

Подробное обсуждение этого R-кода можно найти здесь: «сглаживание» данных времени - можно ли сделать это более эффективно?

0 голосов
/ 30 мая 2011

Что касается вашей второй проблемы, я не уверен насчет PostgreSQL, но в SQL Server есть DATEDIFF (интервал, начальная_дата, конечная_дата), который дает интервал, указанный между двумя датами. Вы можете использовать MIN (Begin) в качестве даты начала и MAX (End) в качестве даты окончания, чтобы получить разницу интервалов. Затем вы можете использовать это в выражении case для вывода чего-либо, хотя вам может потребоваться сделать подзапрос или что-то эквивалентное для вашего сценария.

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