【问题标题】:SQL join 2nd table based on time difference in first tableSQL根据第一个表中的时间差连接第二个表
【发布时间】:2019-01-18 19:04:17
【问题描述】:

我有两张表,一张是活动的开始时间,一张是活动的结束时间,我想加入两者

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

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

例如:

表一:开始时间

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)

谢谢

【问题讨论】:

    标签: mysql sql postgresql join


    【解决方案1】:

    首先,您需要为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_startts_end 之间有任何开始时间(表start_time)时,我们需要ts_end 成为null(第二行)。对于第二行ts_end 必须是NULL,因为5:00 的开始时间介于3:007:00 之间。

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

    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】:

      您必须查看下一个开始是否晚于下一个结束。一种方法使用两个相关的子查询:

      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';
      

      【讨论】:

      • 您好,我正在尝试您对相关子查询的回答,但是您的第一个子查询 (select s.*,...) 在 mysql 中给了我一个错误:错误代码:1242。子查询返回超过1 行?
      【解决方案3】:

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

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

      这样,无论结束时间是否可用,您都将获得开始时间的所有记录。

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

      【讨论】:

      • 我只想以一个正确的结束时间(如果存在)加入每个开始。左连接/外连接会在每个开始时间给出多个结束时间,这不是我想要的
      • 那么应该有一些标志来表示哪个开始时间属于哪个结束时间......你是如何识别的?
      • @Spcoggthesecond 。 . .这些查询中应该有一个limit 1。相反,我只是添加了min()/max() 以获得相同的效果。
      【解决方案4】:

      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);
      

      【讨论】:

        猜你喜欢
        • 2018-05-05
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2018-05-17
        • 1970-01-01
        • 2021-11-11
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多