基于第一张表中的时间差的SQL连接第二张表

时间:2018-08-11 10:35:07

标签: mysql sql postgresql join

我有两个表,一个表保存事件的开始时间,第二个表保存事件的结束时间,我想将两者合并在一起

然而,挑战在于,每个开始事件并不总是有相应的结束事件,如果是的话,我希望输出为NULL。这可能吗?

编辑:每个ID代表一个人,每天可以有多个事件开始和停止。对于每个事件,我只希望将单个“正确”的结束时间加入开始时间(如果存在)。当前没有个人事件级别标识符。

例如:

表1:开始时间

id      ts_start
123     01:00
123     03:00
123     05:00
123     09:00

表2:结束时间

id      ts_end
123     02:00
123     07:00

输出:

id      ts_start    ts_end
123     01:00       02:00
123     03:00       NULL
123     05:00       07:00
123     09:00       NULL

我使用的是MySQL 5.7,因此尚无窗口/分析功能的访问权限,尽管如果这是最佳解决方案的一部分,那么我很乐意进行迁移(不过必须是开源的,因此是新版本的MySQL或Postgres)

谢谢

4 个答案:

答案 0 :(得分:2)

您必须查看下一个起点是否晚于下一个终点。一种方法使用两个相关的子查询:

select id, ts_start,
       (case when next_start > next_end then next_end
        end) as ts_end
from (select s.*,
             (select max(s2.ts_start)
              from starts s2
              where s2.id = s.id and s2.ts_start > s.ts_start
             ) as next_start,
             (select min(e2.ts_end)
              from ends e2
              where e2.id = s.id and e2.ts_end > s.ts_end
             ) as next_end
      from starts s
     ) s;

使用窗口函数,我将所有时间结合在一起,并查看下一个值:

with t as (
      select id, ts_start as time, 'start' as which
      from starts
      union all
      select id, ts_end, 'end'
      from ends
     )
select t.id, t.time as ts_start,
       (case when next_which = 'end' then next_time
        end) as ts_end
from (select t.*,
             lead(time) over (partition by id order by time) as next_time,
             lead(which) over (partition by id order by time) as next_which
      from t
     ) t
where which = 'start';

答案 1 :(得分:2)

首先,您需要为ts_end获取一个“候选”,这是结束时间大于开始时间的最小时间。可以使用

select s.id, s.ts_start, (
  select min(e.ts_end)
  from end_time e
  where e.id = s.id
    and e.ts_end > s.ts_start
) as ts_end
from start_time s;

或搭配

select s.id, s.ts_start, min(e.ts_end) as ts_end
from start_time s
left join end_time e
  on  e.id = s.id
  and e.ts_end > s.ts_start
group by s.id, s.ts_start

两个查询都将返回

|  id | ts_start |   ts_end |
|-----|----------|----------|
| 123 |    01:00 |    02:00 |
| 123 |    03:00 |    07:00 |
| 123 |    05:00 |    07:00 |
| 123 |    09:00 |     null |

现在,当ts_endnull之间有任何开始时间(表start_time)时,我们需要ts_start作为ts_end(第二行)。对于第二行,ts_end必须为NULL,因为开始时间5:003:007:00之间。

对于第一个查询,我们可以使用带有HAVING条件的NOT EXISTS子句:

select s.id, s.ts_start, (
  select min(e.ts_end)
  from end_time e
  where e.id = s.id
    and e.ts_end > s.ts_start
  having not exists (
      select *
      from start_time s2
      where s2.id = s.id
        and s2.ts_start > s.ts_start
        and s2.ts_start < min(e.ts_end)
    )
) as ts_end
from start_time s

第二个查询可以使用CASE表达式和EXISTS条件进行扩展:

select s.id, s.ts_start, 
  case when exists (
      select *
      from start_time s2
      where s2.id = s.id
      and s2.ts_start > s.ts_start
      and s2.ts_start < min(e.ts_end)  
    ) 
    then null
    else min(e.ts_end)
  end as ts_end
from start_time s
left join end_time e
  on  e.id = s.id
  and e.ts_end > s.ts_start
group by s.id, s.ts_start

在MySQL 8.x中,您可以改为使用LEAD窗口函数:

select s.id, s.ts_start,
    case when min(e.ts_end) > lead(s.ts_start) over (partition by s.id order by s.ts_start)
        then null
        else min(e.ts_end)
    end as ts_end
from start_time s
left join end_time e
  on  e.id = s.id
  and e.ts_end > s.ts_start
group by s.id, s.ts_start

所有三个查询都将返回:

|  id | ts_start |   ts_end |
|-----|----------|----------|
| 123 |    01:00 |    02:00 |
| 123 |    03:00 |     null |
| 123 |    05:00 |    07:00 |
| 123 |    09:00 |     null |

演示:https://www.db-fiddle.com/f/6qRaYZKnA7ZYMcTmpZFUwj/0

答案 2 :(得分:0)

在这种情况下,您可以尝试外部联接。例如:

Select st.id, st.ts_start,et.ts_end from startTime st left join endTime et on st.id=et.id;

通过这种方式,无论是否有结束时间,您都将获取开始时间的所有记录。

P.S:只需在查询中输入正确的表名即可。

答案 3 :(得分:0)

1)将当前和下一个ts_start合并到一个查询中:

select
  *, 
  (select min(ts_start) from table1 as tt1 where t1.id = tt1.id and t1.ts_start < tt1.ts_star) as next_start
from table1 as t1;

2)使用table2加入此查询:

select *
from (
  select
    *, 
    (select min(ts_start) from table1 as tt1 where t1.id = tt1.id and t1.ts_start < tt1.ts_star) as next_start
  from table1 as t1) as t1 left join 
    table2 as t2 on (t1.id = t2.id and t2.ts_end between t1.ts_start and t1.next_start);

应该可以在大多数基于SQL的DBMS上使用。

具有简化的数据类型和对象名称的演示:

with 
  t1(x,y) as (values(123,1),(123,3),(123,5),(123,9)),
  t2(x,z) as (values(123,2),(123,7)) 
select *
from (
  select 
    *, 
    (select min(y) from t1 as tt1 where t1.x = tt1.x and t1.y < tt1.y) as next
  from t1) as t1 left join 
    t2 on (t1.x = t2.x and t2.z between t1.y and t1.next);