13

我需要连接表 A 和表 B 来创建表 C。

表 A 和表 B 存储 ID 的状态标志。状态标志(A_Flag 和 B_Flag)可以随时更改,因此一个 ID 可以包含多行,代表 ID 状态的历史记录。特定 ID 的标志可以彼此独立更改,这可能导致表 A 中的一行属于表 B 中的多行,反之亦然。

结果表(表 C)需要是涵盖 ID 生命周期(01/01/2008-18/08/2008)内每个日期的唯一日期范围列表,以及每个日期范围的 A_Flag 和 B_Flag 值。

实际的表包含数百个 ID,每个 ID 在每个表中具有不同数量的行。

我可以使用 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
4

4 回答 4

4

您提出的问题可以在一条 SQL 语句中解决,无需非标准扩展。

要认识到的最重要的事情是,开始-结束对中的每个日期都代表标志对为真的时间跨度的潜在开始或结束点。实际上,一个日期是“开始”,另一个日期是“结束”,这并不重要。任何日期都是一个时间分隔符,它同时完成:它结束前一个时期并开始另一个时期。构造一组最小时间间隔,并将它们连接到表中以查找在每个间隔期间获得的标志。

我将您的示例(和解决方案)添加到我的Canonical SQL页面。有关详细讨论,请参见那里。公平地说,这是查询本身

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
于 2013-03-30T21:15:42.187 回答
4

我将在 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
于 2013-02-25T20:36:46.790 回答
0

这种带有移位和偏移的顺序处理是 SAS DATA 步大放异彩的情况之一。并不是说这个答案很简单,而是它比使用 SQL更简单,可以做到,但在设计时并未考虑到这种顺序处理。

此外,基于 DATA 步的解决方案往往非常有效。这个在理论上运行时间 O(n log n),但在实践中更接近 O(n),并且在恒定空间中。

前两个 DATA 步骤只是加载数据,对 Joe 的回答稍作修改,使其具有多个 ID(否则语法更容易)并添加一些极端情况,即无法确定初始状态的 ID。

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;

下一个数据步骤为每个 id 和 flag 找到第一个修改,并将初始值设置为它找到的相反值。

/* 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;

上面的步骤几乎可以做所有事情。唯一的错误是结束日期是错误的,因为它们是从原始行复制的。要解决此问题,请将下一个开始复制到每一行的末尾,除非它是最后一行。最简单的方法是对每个id进行反向排序,回溯一条记录,最后再升序排序。

/* 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;
于 2013-03-05T03:17:06.533 回答
0

One possible SAS solution to this is to perform a partial join, and then create the necessary additional rows in the data step. This should work assuming tableA has all possible records; if that's not the case (if tableB can start before tableA), some additional logic may be needed to consider that possibility (if first.id and start gt b_start). There may also be additional logic needed for issues not present in the example data - I don't have a lot of time this morning and didn't debug this for anything beyond the example data cases, but the concept should be evident.

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;
于 2013-02-26T16:12:36.263 回答