Сложная кросс-таблица в PostgreSQL, включающая перекрывающиеся даты - PullRequest
0 голосов
/ 10 июля 2019

Я пытаюсь запросить следующие данные:

Student_ID  Site  Start    End       Primary_or_Secondary
1           A     1/1/19   2/28/19   Primary
1           B     2/1/19   6/30/19   Secondary
1           C     3/1/19   6/30/19   Primary

и получить результат, который выглядит следующим образом:

Student_ID  Primary   Secondary  Start   End
1           A         null       1/1/19  1/31/19
1           A         B          2/1/19  2/28/19
1           C         B          3/1/19  6/30/19

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

Я ломал голову над тем, как я могу сделать это в PostgreSQL, иЯ даже посмотрел на функцию кросс-таблицы, но даты мешают моему мозгу: -)

Любая помощь с запросом или набором запросов, включая некоторые CTE, будет очень полезна!

1 Ответ

0 голосов
/ 10 июля 2019

Это не тривиально.Сочетание кросс-таблицы с перекрывающимися и пересекающимися диапазонами, а также угловые случаи (объединение одинаковых дат начала / окончания) сверху.Очень сложно решить с помощью операций, основанных на множествах, т.е. чистого SQL.

Вместо этого я предлагаю процедурное решение в PL / pgSQL.Должно также работать хорошо, поскольку требуется только одно (растровое индексирование) сканирование таблицы:

CREATE OR REPLACE FUNCTION f_student_xtab(VARIADIC _student_ids int[])
  RETURNS TABLE (
      student_id int
    , "primary"  text
    , secondary  text
    , start_date date
    , end_date   date) AS
$func$
DECLARE
   r record;
BEGIN
   student_id := -1; -- init with impossible value

   FOR r IN
      SELECT t.student_id, t.site, t.primary_or_secondary = 'Primary' AS prim, l.range_end, l.date
      FROM   tbl t
      CROSS  JOIN LATERAL (
         VALUES (false, t.start_date)
              , (true , t.end_date)
         ) AS l(range_end, date)
      WHERE  t.student_id = ANY (_student_ids)
      ORDER  BY t.student_id, l.date, range_end -- start of range first
   LOOP
      IF r.student_id <> student_id THEN
         student_id := r.student_id;
         IF r.prim THEN "primary" := r.site;
                   ELSE secondary := r.site;
         END IF;
         start_date := r.date;
      ELSIF r.range_end THEN
         IF r.date < start_date THEN
            -- range already reported
            IF r.prim THEN "primary" := NULL;
                      ELSE secondary := NULL;
            END IF;
            start_date := NULL;
         ELSE
            end_date := r.date;
            RETURN NEXT;
            IF r.prim THEN
               "primary" := NULL;
               IF secondary IS NULL THEN start_date := NULL;
                                    ELSE start_date := r.date + 1;
               END IF;
            ELSE
               secondary := NULL;
               IF "primary" IS NULL THEN start_date := NULL;
                                    ELSE start_date := r.date + 1;
               END IF;
            END IF;
            end_date := NULL;
         END IF;
      ELSE  -- range starts
         IF r.date > start_date THEN
            -- range already running
            end_date := r.date - 1;
            RETURN NEXT;
         END IF;
         start_date := r.date;
         end_date := NULL;
         IF r.prim THEN "primary" := r.site;
                   ELSE secondary := r.site;
         END IF;
      END IF;
   END LOOP;
END
$func$  LANGUAGE plpgsql;

Вызов:

SELECT * FROM f_student_xtab(1,2,3);

Или:

SELECT * FROM f_student_xtab(VARIADIC '{1,2,3}');

db <> fiddle здесь - с расширенным тестовым набором

About VARIADIC:

...