PostgreSQL — объединение / объединение таблиц с разными датами вступления в силу

#postgresql

#PostgreSQL

Вопрос:

У меня есть двумерные таблицы в формате SCD Type 2, которые я хотел бы объединить. Вторая таблица содержит дополнительные строки, относящиеся к конечной структуре первой таблицы.

Первая таблица (foo):

 | employee_id | location_id | team_id | date_effective | date_expired |
|-------------|-------------|---------|----------------|--------------|
| 40          | 1           | 6       | 20180101       | 20190331     |
| 40          | 2           | 6       | 20190331       | 99991231     |
  

Вторая таблица (панель):

 | team_id | manager_id | date_effective | date_expired |
|---------|------------|----------------|--------------|
| 6       | 15         | 20180301       | 20180630     |
| 6       | 27         | 20180630       | 99991231     |
  

Желаемый результат после объединения:

 | employee_id | location_id | team_id | manager_id | date_effective | date_expired |
|-------------|-------------|---------|------------|----------------|--------------|
| 40          | 1           | 6       | NULL       | 20180101       | 20180301     |
| 40          | 1           | 6       | 15         | 20180301       | 20180630     |
| 40          | 1           | 6       | 27         | 20180630       | 20190331     |
| 40          | 2           | 6       | 27         | 20190331       | 99991231     |
  

Я знаю, как объединить две таблицы по датам, но не знаю, как эффективно генерировать дополнительные строки, необходимые для вывода. Вот мой текущий код:

 with foo as (
select * 
from 
(values (40,1,6,20180101,20190331),(40,2,6,20190331,99991231))t(employee_id, location_id, team_id, date_effective, date_expired)
)

,bar as (
select * 
from 
(values (6,15,20180301,20180630),(6,27,20180630,99991231))t(team_id, manager_id, date_effective, date_expired)
)

select * 
from foo f
left join bar b on f.team_id = b.team_id 
    and ((f.date_effective between b.date_effective and b.date_expired)
    or (b.date_effective >= f.date_effective and b.date_effective < f.date_expired))
  

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

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

Ответ №1:

Возможным решением является поиск разных дат как в foo, так и в bar для team_id и объединение обратно в foo и bar .

 with foo as (
select * 
from 
(values (40,1,6,20180101,20190331),(40,2,6,20190331,99991231))t(employee_id, location_id, team_id, date_effective, date_expired)
)
,bar as (
select * 
from 
(values (6,15,20180301,20180630),(6,27,20180630,99991231))t(team_id, manager_id, date_effective, date_expired)
)
,dist as (
select date_effective, team_id
from foo
union
select date_effective, team_id
from bar
)
select *
from dist d
left join foo f on d.team_id = f.team_id and d.date_effective >= f.date_effective and d.date_effective < f.date_expired
left join bar b on d.team_id = b.team_id and d.date_effective >= b.date_effective and d.date_effective < b.date_expired
order by 1
  

Ответ №2:

Я бы попытался решить проблему, создав cte для диапазона before, где в одной из таблиц SCD есть данные, а в другой нет, и перекрывающийся диапазон, где в обеих таблицах есть данные. Аналогично, если бы у меня была ситуация, когда 1 таблица перестала записывать историческую информацию через определенное время, я бы создал диапазон after.

Затем, объединив предыдущие и перекрывающиеся cte, мы получаем желаемый результат.

 WITH foo(employee_id, location_id, team_id, date_effective, date_expired) AS ( VALUES 
  (40,1,6,'2018-01-01'::TIMESTAMP,'2019-03-31'::TIMESTAMP),    
  (40,2,6,'2019-03-31','9999-12-31')    
)
, bar(team_id, manager_id, date_effective, date_expired) AS( VALUES
  (6,15,'2018-03-01'::TIMESTAMP,'2018-06-30'::TIMESTAMP),     
  (6,27,'2018-06-30','9999-12-31')
)
, overlapping AS (
SELECT
  team_id
, employee_id
, location_id
, manager_id
, GREATEST(foo.date_effective, bar.date_effective) date_effective
, LEAST(foo.date_expired, bar.date_expired) date_expired
FROM foo JOIN bar USING (team_id)
WHERE tsrange(foo.date_effective, foo.date_expired) amp;amp; tsrange(bar.date_effective, bar.date_expired)
)
, before AS (
SELECT 
  team_id
, employee_id
, location_id
, NULL::INTEGER manager_id
, MIN(foo.date_effective) date_effective
, MIN(bar.date_effective) date_expired
FROM foo
LEFT JOIN bar USING (team_id)
GROUP BY team_id, employee_id, location_id
HAVING NOT EXISTS (SELECT FROM overlapping WHERE overlapping.date_effective = MIN(foo.date_effective) AND overlapping.team_id = foo.team_id)
)
SELECT * FROM before
UNION ALL
SELECT * FROM overlapping
ORDER BY 5
  

Это дает результат:

 team_id employee_id location_id manager_id  date_effective  date_expired
      6          40           1       NULL      2018-01-01    2018-03-01
      6          40           1         15      2018-03-01    2018-06-30
      6          40           1         27      2018-06-30    2019-03-31
      6          40           2         27      2019-03-31    9999-12-31