【问题标题】:How to find matching time intervals for more than 2 users如何找到超过 2 个用户的匹配时间间隔
【发布时间】:2012-08-11 12:30:28
【问题描述】:

从不同用户的给定时间间隔中找到最合适的时间。

Rows: 5
fid  userid  FromDateTime           ToDateTime          flag
62   1   2012-07-18 01:48:20    2012-07-18 02:55:20     1
63   1   2012-07-18 10:30:46    2012-07-18 12:54:46     1
64   1   2012-07-18 18:50:24    2012-07-18 20:35:24     1
67   1   2012-07-18 15:03:36    2012-07-18 16:03:36     1
68   2   2012-07-18 21:10:47    2012-07-18 23:10:47     1

上表显示了不同用户可用的不同空闲时间段,例如:

user1

中是免费的
2012-07-18 01:48:20   to   2012-07-18 02:55:20 , 
2012-07-18 10:30:46   to   2012-07-18 12:54:46 
......

user 2 仅在这段时间内免费:

2012-07-18 21:10:47   to   2012-07-18 23:10:47 

现在我想找出两个用户都可以安排会议的最佳时间间隔。

【问题讨论】:

  • 欢迎来到 SO! ...您自己尝试过什么? =)
  • 你必须定义“最佳”背后的逻辑。
  • 显然,最好的时间是MAX(ToDateTime)之后的任何时间:)
  • 我知道我必须定义逻辑,但逻辑应该是什么?
  • 您是要查找 user1 和 user2 何时空闲?

标签: mysql computational-geometry


【解决方案1】:

要查找 user1 和 user2 何时空闲,请尝试以下操作:

select 
a.datetime_start as user1start,a.datetime_end as user1end,
b.datetime_start as user2start,b.datetime_end as user2end ,
case when a.datetime_start > b.datetime_start then a.datetime_start 
   else b.datetime_start end as avail_start,
case when a.datetime_end>b.datetime_end then b.datetime_end 
   else a.datetime_end end as avail_end
from users a inner join users b on
a.datetime_start<=b.datetime_end and a.datetime_end>=b.datetime_start     
and  a.userid={user1} and b.userid={user2}

SQL FIDDLE HERE.

编辑: 如需比较超过 2 个用户,请尝试以下操作:

select max(datetime_start) as avail_start,min(datetime_end) as avail_end
from(
        select *,
        @rn := CASE WHEN @prev_start <=datetime_end and @prev_end >=datetime_start THEN @rn ELSE @rn+1 END AS rn,
        @prev_start := datetime_start,
        @prev_end := datetime_end 
        from(
          select * from users2 m
          where exists ( select null 
                          from users2 o 
                           where o.datetime_start <= m.datetime_end and o.datetime_end >= m.datetime_start
                           and o.id <> m.id 
                        ) 
             and m.userid in (2,4,3,5)
           order by m.datetime_start) t,
           (SELECT @prev_start := -1, @rn := 1, @prev_end=-1) AS vars 
) c 
group by rn 
having count(rn)=4 ;

需要根据用户数量更改m.userid in (2,4,3,5)having count(rn)=4

SQL FIDDLE HERE

【讨论】:

  • 非常感谢 .. 很好的解决方案。但这仅在我能找到的 2 个用户之间......我的要求是在超过 2 个用户之间找到。它可以是 2 或 3 或 5 ..... 用户数不固定
  • @HemantAgrawal,为超过 2 个用户的场景编辑了我的答案。请尝试。如果答案有帮助,请接受答案。谢谢。
  • 非常感谢.. SEL。但我真的很抱歉我没有得到任何结果。
  • 在我的表中存在 3 个不同的用户。用户 1 有 3 个不同的空闲时间可用,用户 2 和用户 3 只有一个空闲时间条目。当我从查询中删除有子句时,我可以看到结果返回 4 条不同的记录。但是有子句的结果是空的。
  • 我认为有像 'have rn=4' 这样的子句不是 count(rn)。我做这个改变。现在我有了结果,但这不是真的。您为 2 个用户发布的第一个查询只给了我完美的结果。但是对 2 个用户的相同查询没有给出正确的结果。两个查询结果不同。
【解决方案2】:

您可以使用此解决方案找到符合您条件的所有 用户(例如userids 1-5)可以满足的“最佳”时间窗口。 “最佳”时间窗口以最大秒数衡量。

SELECT   MAX(b.FromDateTime) FromDateTime, 
         a.ToDateTime
FROM     (
         SELECT   DISTINCT a.ToDateTime
         FROM     tbl a
         JOIN     tbl b ON a.userid     <> b.userid
                       AND a.userid     IN (1,2,3,4,5)
                       AND b.userid     IN (1,2,3,4,5)
                       AND a.ToDateTime >  b.FromDateTime 
                       AND a.ToDateTime <= b.ToDateTime
         GROUP BY a.userid,
                  a.FromDateTime,
                  a.ToDateTime
         HAVING   COUNT(DISTINCT b.userid) = 4
         ) a
JOIN     (
         SELECT   DISTINCT a.FromDateTime
         FROM     tbl a
         JOIN     tbl b ON a.userid       <> b.userid
                       AND a.userid       IN (1,2,3,4,5)
                       AND b.userid       IN (1,2,3,4,5)
                       AND a.FromDateTime >= b.FromDateTime 
                       AND a.FromDateTime <  b.ToDateTime
         GROUP BY a.userid,
                  a.FromDateTime,
                  a.ToDateTime
         HAVING   COUNT(DISTINCT b.userid) = 4
         ) b ON b.FromDateTime < a.ToDateTime
GROUP BY a.ToDateTime
ORDER BY TIMESTAMPDIFF(SECOND, MAX(b.FromDateTime), a.ToDateTime) DESC
LIMIT    1

COUNT(DISTINCT... 之后的 4 只是您的条件中的用户数减一(因为用户无法加入自己)。相应调整。

我们应该返回的是所有用户都可以参加的会议的开始和结束时间。


查询细分


给定以下数据:

(62, 1, '2012-07-18 00:00:00', '2012-07-18 12:00:00', 1),

(63, 2, '2012-07-18 00:00:00', '2012-07-18 02:00:00', 1),
(64, 2, '2012-07-18 03:00:00', '2012-07-18 05:00:00', 1),
(65, 2, '2012-07-18 05:30:00', '2012-07-18 06:00:00', 1),

(66, 3, '2012-07-18 00:30:00', '2012-07-18 02:30:00', 1),
(67, 3, '2012-07-18 03:10:00', '2012-07-18 07:30:00', 1),

(68, 4, '2012-07-18 01:10:00', '2012-07-18 03:20:00', 1),
(69, 4, '2012-07-18 03:50:00', '2012-07-18 06:00:00', 1),

(70, 5, '2012-07-18 01:10:00', '2012-07-18 03:20:00', 1),
(71, 5, '2012-07-18 04:30:00', '2012-07-18 07:10:00', 1),


(72, 1, '2012-07-18 13:00:00', '2012-07-18 14:00:00', 1),
(73, 2, '2012-07-18 13:30:00', '2012-07-18 14:30:00', 1),
(74, 3, '2012-07-18 14:00:00', '2012-07-18 15:00:00', 1),
(75, 4, '2012-07-18 14:30:00', '2012-07-18 15:30:00', 1),
(76, 5, '2012-07-18 18:00:00', '2012-07-18 19:00:00', 1);

相对时间间隔位置应如下图所示(必须横向滚动才能看到全部内容):

uid 1   <--------------------------------------------------------------------------------------...-------->      <-------------------->
uid 2   <----------------------->          <----------------------->    <---->                                          <-------------------->
uid 3       <----------------------->       <------------------------------------------->                                       <-------------------->
uid 4                 <----------------------->      <----------------------->                                                         <-------------------->
uid 5                 <----------------------->              <----------------------->                                                                                              <-------------------->
                      [    1    ]           [2]              [  3  ]    [ 4  ]
                           ^
       We want the start and end times of this overlap

括号中的数字[]代表所有用户空闲时间重叠的时间窗口。我们想要重叠#1,因为它是最长的。重叠 #1 应该是 2012-07-18 1:10:002012-07-18 2:00:00,所以我们的预期结果应该是:

FromDateTime       | ToDateTime
----------------------------------------
2012-07-18 1:10:00 | 2012-07-18 2:00:00

第 1 步:

我们必须做的第一件事是弄清楚所有潜在会议窗口的结束时间。我们通过选择其结束时间在所有其他用户的空闲时间间隔之间的特定间隔来做到这一点。

返回的结束时间表示上面文字插图中指出的每个重叠的结束时间。如果返回了两个相同的结束时间,我们只选择一个,因为我们不需要知道关于该结束时间的任何其他信息,除了这是该特定会议可以进行到的最晚时间这一事实:

SELECT   DISTINCT a.ToDateTime
FROM     tbl a
JOIN     tbl b ON a.userid     <> b.userid
              AND a.userid     IN (1,2,3,4,5)
              AND b.userid     IN (1,2,3,4,5)
              AND a.ToDateTime >  b.FromDateTime 
              AND a.ToDateTime <= b.ToDateTime
GROUP BY a.userid,
         a.FromDateTime,
         a.ToDateTime
HAVING   COUNT(DISTINCT b.userid) = 4

渲染:

TODATETIME
-------------------
2012-07-18 02:00:00
2012-07-18 05:00:00
2012-07-18 06:00:00
2012-07-18 03:20:00

SQLFiddle Demo


第 2 步:

接下来我们要做的是与上一步相反,找出每个潜在会议窗口的所有开始时间,然后加入在开始时间小于上一步的结束时间的条件下,此查询的结果与上一步的结果:

SELECT   b.FromDateTime,
         a.ToDateTime
FROM     (
         SELECT   DISTINCT a.ToDateTime
         FROM     tbl a
         JOIN     tbl b ON a.userid     <> b.userid
                       AND a.userid     IN (1,2,3,4,5)
                       AND b.userid     IN (1,2,3,4,5)
                       AND a.ToDateTime >  b.FromDateTime 
                       AND a.ToDateTime <= b.ToDateTime
         GROUP BY a.userid,
                  a.FromDateTime,
                  a.ToDateTime
         HAVING   COUNT(DISTINCT b.userid) = 4
         ) a
JOIN     (
         SELECT   DISTINCT a.FromDateTime
         FROM     tbl a
         JOIN     tbl b ON a.userid       <> b.userid
                       AND a.userid       IN (1,2,3,4,5)
                       AND b.userid       IN (1,2,3,4,5)
                       AND a.FromDateTime >= b.FromDateTime 
                       AND a.FromDateTime <  b.ToDateTime
         GROUP BY a.userid,
                  a.FromDateTime,
                  a.ToDateTime
         HAVING   COUNT(DISTINCT b.userid) = 4
         ) b ON b.FromDateTime < a.ToDateTime
ORDER BY a.ToDateTime, b.FromDateTime --Ordered for display purposes

渲染:

TODATETIME          | FROMDATETIME        
------------------------------------------
2012-07-18 02:00:00 | 2012-07-18 01:10:00  <-- Most recent FromDateTime
2012-07-18 03:20:00 | 2012-07-18 01:10:00 
2012-07-18 03:20:00 | 2012-07-18 03:10:00  <-- Most recent FromDateTime
2012-07-18 05:00:00 | 2012-07-18 01:10:00 
2012-07-18 05:00:00 | 2012-07-18 03:10:00 
2012-07-18 05:00:00 | 2012-07-18 04:30:00  <-- Most recent FromDateTime 
2012-07-18 06:00:00 | 2012-07-18 01:10:00 
2012-07-18 06:00:00 | 2012-07-18 03:10:00 
2012-07-18 06:00:00 | 2012-07-18 04:30:00 
2012-07-18 06:00:00 | 2012-07-18 05:30:00  <-- Most recent FromDateTime 

最近的FromDateTimes 代表每个潜在会议窗口的开始。我们只想根据ToDateTime 提取FromDateTime 最新的行。我们在下一步中使用GROUP BYMAX() 聚合函数来执行此操作。

SQLFiddle Demo


第三步:

接下来,我们在ToDateTime 上使用GROUP BY 并在FromDateTime 上使用MAX() 来仅提取最新的FromDateTimes

SELECT   MAX(b.FromDateTime) FromDateTime, 
         a.ToDateTime
FROM     (
         SELECT   DISTINCT a.ToDateTime
         FROM     tbl a
         JOIN     tbl b ON a.userid     <> b.userid
                       AND a.userid     IN (1,2,3,4,5)
                       AND b.userid     IN (1,2,3,4,5)
                       AND a.ToDateTime >  b.FromDateTime 
                       AND a.ToDateTime <= b.ToDateTime
         GROUP BY a.userid,
                  a.FromDateTime,
                  a.ToDateTime
         HAVING   COUNT(DISTINCT b.userid) = 4
         ) a
JOIN     (
         SELECT   DISTINCT a.FromDateTime
         FROM     tbl a
         JOIN     tbl b ON a.userid       <> b.userid
                       AND a.userid       IN (1,2,3,4,5)
                       AND b.userid       IN (1,2,3,4,5)
                       AND a.FromDateTime >= b.FromDateTime 
                       AND a.FromDateTime <  b.ToDateTime
         GROUP BY a.userid,
                  a.FromDateTime,
                  a.ToDateTime
         HAVING   COUNT(DISTINCT b.userid) = 4
         ) b ON b.FromDateTime < a.ToDateTime
GROUP BY a.ToDateTime

渲染:

FROMDATETIME        | TODATETIME
-----------------------------------------
2012-07-18 01:10:00 | 2012-07-18 02:00:00
2012-07-18 03:10:00 | 2012-07-18 03:20:00
2012-07-18 04:30:00 | 2012-07-18 05:00:00
2012-07-18 05:30:00 | 2012-07-18 06:00:00

这些基本上是我们潜在的时间窗口。现在只需选择最长的一个即可。


第四步:

我们使用ORDER BY / LIMIT 1 最大/最小选择技术,因为我们只需要一行。我们根据每次会议的结束时间和开始时间之间的秒差进行排序,然后选择秒数最多的一个(通过LIMIT 1),得到我们最终想要的结果:

SELECT   MAX(b.FromDateTime) FromDateTime, 
         a.ToDateTime
FROM     (
         SELECT   DISTINCT a.ToDateTime
         FROM     tbl a
         JOIN     tbl b ON a.userid     <> b.userid
                       AND a.userid     IN (1,2,3,4,5)
                       AND b.userid     IN (1,2,3,4,5)
                       AND a.ToDateTime >  b.FromDateTime 
                       AND a.ToDateTime <= b.ToDateTime
         GROUP BY a.userid,
                  a.FromDateTime,
                  a.ToDateTime
         HAVING   COUNT(DISTINCT b.userid) = 4
         ) a
JOIN     (
         SELECT   DISTINCT a.FromDateTime
         FROM     tbl a
         JOIN     tbl b ON a.userid       <> b.userid
                       AND a.userid       IN (1,2,3,4,5)
                       AND b.userid       IN (1,2,3,4,5)
                       AND a.FromDateTime >= b.FromDateTime 
                       AND a.FromDateTime <  b.ToDateTime
         GROUP BY a.userid,
                  a.FromDateTime,
                  a.ToDateTime
         HAVING   COUNT(DISTINCT b.userid) = 4
         ) b ON b.FromDateTime < a.ToDateTime
GROUP BY a.ToDateTime
ORDER BY TIMESTAMPDIFF(SECOND, MAX(b.FromDateTime), a.ToDateTime) DESC
LIMIT    1

SQLFiddle Demo of Final Result

SQLFiddle Demo with Other Example Data


获取表中所有用户的会议时间(无条件):

如果您不想指定要检查哪些用户的会议时间(只需为表中的所有用户执行此操作),您可以使用:

SELECT   MAX(b.FromDateTime) FromDateTime, 
         a.ToDateTime
FROM     (
         SELECT     DISTINCT a.ToDateTime
         FROM       tbl a
         JOIN       tbl b ON a.userid     <> b.userid
                         AND a.ToDateTime >  b.FromDateTime 
                         AND a.ToDateTime <= b.ToDateTime
         CROSS JOIN (SELECT COUNT(DISTINCT userid) totalusers FROM tbl) c
         GROUP BY   a.userid,
                    a.FromDateTime,
                    a.ToDateTime,
                    c.totalusers
         HAVING     COUNT(DISTINCT b.userid) = c.totalusers-1
         ) a
JOIN     (
         SELECT   DISTINCT a.FromDateTime
         FROM     tbl a
         JOIN     tbl b ON a.userid       <> b.userid
                       AND a.FromDateTime >= b.FromDateTime 
                       AND a.FromDateTime <  b.ToDateTime
         CROSS JOIN (SELECT COUNT(DISTINCT userid) totalusers FROM tbl) c
         GROUP BY a.userid,
                  a.FromDateTime,
                  a.ToDateTime,
                  c.totalusers
         HAVING   COUNT(DISTINCT b.userid) = c.totalusers-1
         ) b ON b.FromDateTime < a.ToDateTime
GROUP BY a.ToDateTime
ORDER BY TIMESTAMPDIFF(SECOND, MAX(b.FromDateTime), a.ToDateTime) DESC
LIMIT    1

【讨论】:

  • 一个优雅的解决方案实现,返回最大的成对空闲时间。但所有用户时间的交集不一定是这些结果。假设每个人从12:00到12:15有空,user1和user2从1:00到2:00有空,user3和user4从3:00到4:00有空,以此类推。从12开始的时间间隔: 00 不会包含在您的结果中。 :(
  • @EthanB,我不确定我是否完全理解。我刚刚用您的数据进行了测试,结果符合预期。请参阅sqlfiddle.com/#!2/0a172/2 - 这是非重复解决方案。用户12 见面时间为 1:00 至 2:00。是的,他们可以在 12:00 到 12:15 见面,但这不包括在用户 ID 12 的结果中,因为这不是最大时间窗口:1: 00-2:00 是。对于用户 ID 34 也是如此。或许 OP 想要找到每天的最佳会议时间?由于13,4 之间的唯一重叠时间间隔是 12:00-12:15,因此该时间间隔加入。
  • OP 希望能够查看 超过 2 个用户的最佳会议时间。
  • @EthanB,好的,所以不仅仅是同时多对,而是所有用户可以同时见面的时间范围。明白了。
  • @ZaneBien,如ethanB所示,OP希望找到超过2个用户的共同重叠时间。
【解决方案3】:

我使用扫描线 (Wikipedia) 在 PHP 中创建了一个一维线段相交算法。它之所以有效,是因为日期时间可以映射到数字线:例如使用“自纪元以来的毫秒数”。

在此处查看实现:http://pastebin.com/iLwJQEF0

该算法输出一个线段交叉点数组(也是线段),其中还包含在持续时间内可用的所有用户的列表。您可以根据“最佳”的定义对交叉点进行排序(并将其反转为降序):首先按可用用户的数量,然后按他们的持续时间。 (已经实现了!)

它在O(n * log n) 中运行,其中n 是时间段数。

注意事项:

  • 如果您不想搞乱日期时间到毫秒的转换,可以替换减法和大于/小于运算符。 (我给你留了一些cmets。)
  • 注意在同一位置开始/结束的线段很重要:
    • 扫描线必须遇到端点之前相同值的起点。
    • 另请注意,当两个线段以相同的值结束时,它不会产生无关的结果。
  • 我确信这可以在数据库引擎中重新实现(如果您认为值得的话)。许多数据库供应商都有几何扩展。

【讨论】:

    【解决方案4】:

    我找到了一种 hacky 方法来做到这一点:

    Perl 有一个叫做Set::IntSpan 的东西,它有一个intersect 函数(或方法),可以找到两个数字间隔的公共范围。我们的想法是利用它。

    您可以在 php.ini 中使用 strtotime("2012-08-27 02:02:02") 将日期时间字符串转换为时间戳(数字)。一旦你有了两对时间戳,你就可以使用下面的示例 perl 代码来查找相交区间,从中可以找到时间。

    use Set::IntSpan;
    
    my $r1 = Set::IntSpan->new([ 5 .. 15 ]);
    my $r2 = Set::IntSpan->new([ 2 .. 20 ]);
    
    my $i = $r1->intersect($r2);
    
    if ( !$i->empty and ( $i->max - $i->min ) >= 5 ) # criteria
    {
    print "hit\n"; # $i->max, $i->min are the timestamps you need
    }
    else
    {
    print "miss\n";
    }
    

    一旦有了相交间隔,您就可以使用date("Y-m-d H:i:s", $timestamp);从时间戳(如果需要)取回日期时间

    以下是一些相关链接和参考资料:

    Calculate overlap between 2 ranges of numbers

    Calling Perl script from PHP and passing in variables, while also using variablized perl script name

    附言也许 perl 专业人士可以将代码包装成一个有 4 个参数的函数?另外,我知道这不是问题的完美答案,但我认为这个想法很酷。

    【讨论】:

      【解决方案5】:

      使用小提琴中的 sel 架构 (10x sel)...

      最简单的方法是:

      SELECT
          MAX(GREATEST(u1.datetime_start, u2.datetime_start)) AS MeetingStart,
          MIN(LEAST(u1.datetime_end, u2.datetime_end)) AS MeetingEnd
      FROM users2 u1
      INNER JOIN users2 u2
          ON (u1.datetime_end >= u2.datetime_start AND u1.datetime_start <= u2.datetime_end)
          AND u2.userid != u1.userid
          AND u2.userid IN (3,4,5)
      WHERE u1.userid=2
      GROUP BY u1.id
      HAVING COUNT(DISTINCT u2.userid) = 3 AND MeetingStart < MeetingEnd
      

      根据你的情况改变:

      在我的示例中,我们有 4 个参与者。 n=4,参与者(2,3,4,5)

      IN (3,4,5) --> 参加会议的最后 n-1 个 id

      WHERE u1.userid=2 --> 会议第一个参与者的 ID

      拥有 COUNT(DISTINCT u2.userid) = 3 --> n - 1

      可以在sqlfiddle上测试

      【讨论】:

      • 我用IN (2,4,5)IN (2,3,4,5) 试试你的sqlfiddle,它没有返回想要的结果。
      • hmm.. 你是否相应地更改了所有参数?在、在哪里和拥有?
      • 我的错误。您的解决方案确实有效。但是该解决方案不适用于此测试数据。 sqlfiddle.com/#!2/23e55/1 我修改为('4', '2012-08-01 17:00:00', ' 2012-08-01 19:00:00 ') 并添加('4', '2012-08-01 19:10:00', ' 2012-08-01 20:00:00 ')。结果返回不包括 2012-08-01。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2014-10-26
      • 1970-01-01
      • 2011-05-07
      • 2013-03-25
      • 1970-01-01
      • 2019-10-08
      • 1970-01-01
      相关资源
      最近更新 更多