2013-02-25 9 views
11

Мне нужно соединить таблицу A и таблицу B, чтобы создать таблицу C.Сопоставление перекрывающихся диапазонов дат

Таблица A и таблица B сохраняют флаги состояния для идентификаторов. Флаги состояния (A_Flag и B_Flag) могут время от времени меняться, поэтому один ID может содержать несколько строк, которые представляют историю статусов идентификатора. Флаги для определенного идентификатора могут изменяться независимо друг от друга, что может привести к тому, что одна строка в таблице А будет принадлежать нескольким строкам в таблице B и наоборот.

Результирующая таблица (таблица C) должна быть списком уникальных диапазонов дат, охватывающих каждую дату жизни ID (01/01/2008-18/08/2008) и значения A_Flag и B_Flag для каждого диапазона дат ,

Фактические таблицы содержат сотни идентификаторов с каждым идентификатором, имеющим различное количество строк в таблице.

У меня есть доступ к инструментам SQL и SAS для достижения конечного результата.

Source - Table A 
ID Start   End  A_Flag 
1 01/01/2008 23/03/2008 1 
1 23/03/2008 15/06/2008 0 
1 15/06/2008 18/08/2008 1 

Source - Table B 
ID Start   End  B_Flag 
1 19/01/2008 17/02/2008 1 
1 17/02/2008 15/06/2008 0 
1 15/06/2008 18/08/2008 1 

Result - Table C 
ID Start   End A_Flag B_Flag 
1 01/01/2008 19/01/2008 1 0 
1 19/01/2008 17/02/2008 1 1 
1 17/02/2008 23/03/2008 1 0 
1 23/03/2008 15/06/2008 0 0 
1 15/06/2008 18/08/2008 1 1 
+0

Я не могу придумать способ сделать это только с помощью стандартного SQL, и я не знаю SAS. Если бы я знал, какой вкус, я должен был бы написать процедуру, которая будет работать. – pahoughton

+0

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

ответ

3

Я собираюсь решить эту проблему в SQL, при условии, что у вас есть функция под названием lag (SQL Server 2012, Oracle, Postgres, DB2). Вы можете получить тот же эффект с коррелированным подзапросом.

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

У меня возникли проблемы с загрузкой кода, но вы можете получить большую часть его. Тем не менее, он начинается с начальных концов, которые вы создаете, делая union (не union all) четырех дат в одном столбце: выберите a.start в качестве даты. Затем он объединяется с a.end, b.start и b.end.

with driver as (
    select thedate as start, lag(thedate) over (order by thedate) as end 
    from startends 
    )  

select startdate, enddate, a.flag, b.flag 
from driver left outer join 
    a 
    on a.start >= driver.start and a.end <= driver.end left outer join 
    b 
    on b.start >= driver.start and b.end <= driver.end 
+0

Гордон, спасибо за это. У меня есть только доступ к синтаксису SQL через PROC SQL (процедура в SAS), поэтому возможен коррелированный подзапрос, но не функция задержки. Как эта пчела может быть выполнена с использованием коррелированного подзапроса? – geebees

+0

Если у вас есть базовая база данных, например Oracle (которая поддерживает задержку), вы можете настроить сквозной режим и поместить в синтаксис Oracle. Возможно ли это? –

+0

У SAS есть функция задержки, я думаю, мне придется поэкспериментировать с ней, чтобы достичь желаемого результата – geebees

0

Одним из возможных решений SAS для этого является выполнение частичного соединения, а затем создание необходимых дополнительных строк на этапе данных. Это должно работать, если таблица А имеет все возможные записи; если это не так (если tableB может запускаться перед таблицей A), для рассмотрения этой возможности может потребоваться дополнительная логика (если first.id и start gt b_start). Также может потребоваться дополнительная логика для проблем, отсутствующих в данных примера. У меня нет большого количества времени этим утром и не отлаживал это для чего-либо за пределами примеров данных, но концепция должна быть очевидной.

data tableA; 
informat start end DDMMYY10.; 
format start end DATE9.; 
input ID Start   End  A_Flag; 
datalines; 
1 01/01/2008 23/03/2008 1 
1 23/03/2008 15/06/2008 0 
1 15/06/2008 18/08/2008 1 
;;;; 
run; 

data tableB; 
informat start end DDMMYY10.; 
format start end DATE9.; 
input ID Start   End  B_Flag; 
datalines; 
1 19/01/2008 17/02/2008 1 
1 17/02/2008 15/06/2008 0 
1 15/06/2008 18/08/2008 1 
;;;; 
run; 


proc sql; 
create table c_temp as 
    select * from tableA A 
     left join (select id, start as b_start, end as b_end, b_flag from tableB) B 
    on A.Id = B.id 
    where (A.start le B.b_start and A.end gt B.b_start) or (A.start lt B.b_end and A.end ge B.b_end) 
    order by A.ID, A.start, B.b_start; 
quit; 

data tableC; 
set c_temp; 
by id start; 
retain b_flag_ret; 
format start_fin end_fin DATE9.; 
if first.id then b_flag_ret=0; 
do until (start=end); 
    if (start lt b_start) and first.start then do; 
     start_fin=start; 
     end_fin=b_start; 
     a_flag_fin=a_flag; 
     b_flag_fin=b_flag_ret; 
     output; 
     start=b_start; 
    end;  
    else do; *start=b_start; 
      start_fin=ifn(start ge b_start, start, b_start); 
      end_fin = ifn(b_end le end, b_end, end); 
      a_flag_fin=a_flag; 
      b_flag_fin=b_flag; 
      output; 
      start=end; *leave the loop as there will be a later row that matches; 
    end; 
end; 
run; 
0

Этот тип последовательной обработки со сдвигами и взаимозачетов одна из ситуаций, в которых шаг DATA SAS блестит. Не то, чтобы этот ответ прост, но это проще, чем использование SQL, что можно сделать, но не рассчитано на эту последовательную обработку.

Кроме того, решения, основанные на шаге DATA, имеют тенденцию быть очень эффективными. Это выполняется во времени O (n log n) в теории, но ближе к O (n) на практике и в постоянном пространстве.

Первые два этапа DATA просто загружают данные, слегка модифицированные из ответа Джо, для того, чтобы иметь несколько идентификаторов (иначе синтаксис намного проще) и добавить некоторые угловые случаи, то есть идентификатор, для которого невозможно определить начальное состояние.

data tableA; 
informat start end DDMMYY10.; 
format start end DATE9.; 
input ID Start   End  A_Flag; 
datalines; 
1 01/01/2008 23/03/2008 1 
2 23/03/2008 15/06/2008 0 
2 15/06/2008 18/08/2008 1 
;;;; 
run; 

data tableB; 
informat start end DDMMYY10.; 
format start end DATE9.; 
input ID Start   End  B_Flag; 
datalines; 
1 19/01/2008 17/02/2008 1 
2 17/02/2008 15/06/2008 0 
4 15/06/2008 18/08/2008 1 
;;;; 
run; 

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

/* Get initial state by inverting first change */ 
data firstA; 
    set tableA; 
    by id; 
    if first.id; 
    A_Flag = ~A_Flag; 
run; 

data firstB; 
    set tableB; 
    by id; 
    if first.id; 
    B_Flag = ~B_Flag; 
run; 
data first; 
    merge firstA firstB; 
    by id; 
run; 

Следующий шаг данных объединяет искусственную «первую» таблицу с двумя другими, сохраняя последнее состояние, известное и отбрасывание искусственного исходного ряда.

data tableAB (drop=lastA lastB); 
    set first tableA tableB; 
    by id start; 
    retain lastA lastB lastStart; 
    if A_flag = . and ~first.id then A_flag = lastA; 
    else lastA = A_flag; 
    if B_flag = . and ~first.id then B_flag = lastB; 
    else lastB = B_flag; 
    if ~first.id; /* drop artificial first row per id */ 
run; 

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

/* sort descending to ... */ 
proc sort data=tableAB; 
    by id descending start; 
run; 
/* ... copy next start to this row's "end" field if not final */ 
data tableAB(drop=nextStart); 
    set tableAB; 
    by id descending start; 
    nextStart=lag(start); 
    if ~first.id then end=nextStart; 
run; 

proc sort data=tableAB; 
    by id start; 
run; 
3

Задача, которую вы задали, может быть решена в одном выражении SQL без нестандартных расширений.

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

Я добавил ваш пример (и решение) на мою страницу Canonical SQL. См. Там подробное обсуждение. В справедливости к SO, вот сам запрос

with D (ID, bound) as (
    select ID 
     , case T when 's' then StartDate else EndDate end as bound 
    from (
    select ID, StartDate, EndDate from so.A 
    UNION 
    select ID, StartDate, EndDate from so.B 
    ) as U 
    cross join (select 's' as T union select 'e') as T 
) 
select P.*, a.Flag as A_Flag, b.Flag as B_Flag 
from (
    select s.ID, s.bound as StartDate, min(e.bound) as EndDate 
    from D as s join D as e 
    on s.ID = e.ID 
    and s.bound < e.bound 
    group by s.ID, s.bound 
) as P 
left join so.A as a 
on P.ID = a.ID 
and a.StartDate <= P.StartDate and P.EndDate <= a.EndDate 
left join so.B as b 
on P.ID = b.ID 
and b.StartDate <= P.StartDate and P.EndDate <= b.EndDate 
order by P.ID, P.StartDate, P.EndDate 
Смежные вопросы