【问题标题】:PHP/MYSQL datetime ranges overlapping for users用户的 PHP/MYSQL 日期时间范围重叠
【发布时间】:2012-04-15 08:28:58
【问题描述】:

请我在这方面需要帮助(为了更好地理解,请参阅附图),因为我完全无能为力。

如您所见,我有用户,他们将他们的开始和结束日期时间存储在我的数据库中,格式为 YYYY-mm-dd H:i:s。现在我需要根据最频繁的时间范围重叠(对于大多数用户)找出所有用户的重叠。我想为大多数用户获得 3 个最频繁的数据时间重叠。我该怎么做?

我不知道我应该使用哪个 mysql 查询,或者最好从数据库中选择所有日期时间(开始和结束)并在 php 中处理它(但是如何处理?)。如图像结果所述,例如时间 8.30 - 10.00 是用户 A+B+C+D 的结果。

Table structure:
UserID | Start datetime | End datetime
--------------------------------------
A | 2012-04-03 4:00:00 | 2012-04-03 10:00:00
A | 2012-04-03 16:00:00 | 2012-04-03 20:00:00
B | 2012-04-03 8:30:00 | 2012-04-03 14:00:00
B | 2012-04-06 21:30:00 | 2012-04-06 23:00:00
C | 2012-04-03 12:00:00 | 2012-04-03 13:00:00
D | 2012-04-01 01:00:01 | 2012-04-05 12:00:59
E | 2012-04-03 8:30:00 | 2012-04-03 11:00:00
E | 2012-04-03 21:00:00 | 2012-04-03 23:00:00

【问题讨论】:

  • 请贴出表格结构。你想在当天/周/月/年/所有时间运行它吗?
  • 从我的角度来看,如果它是当前日期/星期/等并不重要。因为例如用户 D 可以有开始日期时间 2012-04-01 01:00:01 和结束日期时间 2012-04-05 12:00:59,而用户 A 开始日期时间 2012-04-03 4:00:00 和结束日期时间 2012-04-03 10:00:00,用户 B 开始 2012-04-03 8:30:00 和结束 2012-04-03 14:00:00 但我的脚本应该检测这些用户的范围重叠作为开始 2012 -04-03 8:30:00 和 2012-04-03 10:00:00 结束用户 A+B+D。
  • 以下示例基于为当天创建时间段而工作。这应该足以让您朝着正确的方向开始。
  • 如果它们只重叠一秒怎么办?还是仅相差 3 微秒(在支持更高分辨率时间戳的不同 dbms 上)?
  • 我还没有遇到过将约会管理到微秒级精度的人 ;) 尽管使用我提出的解决方案,您确实遇到了在 time_slot 上开始和结束预订的问题。这只是一个粗略的例子,让 OP 思考如何解决他的问题。

标签: php mysql datetime


【解决方案1】:

您实际上拥有的是一组集合,并且想要确定其中是否有任何非零交集。当试图在嵌套集中查找节点的所有祖先时,这正是人们要问的问题。

我们可以证明,对于每个重叠,至少有一个时间窗口的开始时间会落在所有其他重叠时间窗口内。使用这个花絮,我们不需要在一天中实际构建人工时隙。只需计算一个开始时间,看看它是否与任何其他时间窗口相交,然后计算相交的数量。

那么查询是什么?

/*SELECT*/
SELECT DISTINCT
    MAX(overlapping_windows.start_time) AS overlap_start_time,
    MIN(overlapping_windows.end_time) AS overlap_end_time ,
    (COUNT(overlapping_windows.id) - 1) AS num_overlaps
FROM user_times AS windows
INNER JOIN user_times AS overlapping_windows
ON windows.start_time BETWEEN overlapping_windows.start_time AND overlapping_windows.end_time
GROUP BY windows.id
ORDER BY num_overlaps DESC;

根据您的表大小以及您计划运行此查询的频率,可能值得在其上删除空间索引(见下文)。

更新

如果您经常运行此查询,则需要使用空间索引。由于基于范围的遍历(即 start_time 是否在开始/结束范围之间),BTREE 索引不会为您做任何事情。它必须是空间的。

ALTER TABLE user_times ADD COLUMN time_windows GEOMETRY NOT NULL DEFAULT 0;
UPDATE user_times SET time_windows = GeomFromText(CONCAT('LineString( -1 ', start_time, ', 1 ', end_time, ')'));
CREATE SPATIAL INDEX time_window ON user_times (time_window);

然后你可以更新上面查询中的ON子句来读取

ON MBRWithin( Point(0,windows.start_time), overlapping_windows.time_window )

这将为您提供查询的索引遍历。同样,仅当您计划经常运行查询时才这样做。

空间索引归功于Quassoni's blog

【讨论】:

  • 我认为这是一个有趣的答案,所以我用 SQL Fiddle 尝试了它,但它并没有完全产生预期的结果 - sqlfiddle.com/#!2/2aad9/1/0 - 也许稍微调整一下就可以了。
  • 这是一个很酷的小工具!只需将其设为 SELECT DISTINCT 即可消除重复项(已更新原始帖子)。此外,总计数为 0 索引,但如果您想要自然编号,您可以从 COUNT 语句中删除负 1 并添加 HAVING num_overlaps > 1。 sqlfiddle.com/#!2/2aad9/9
  • 如果您对重叠时间窗口的数量感兴趣,请在 COUNT 中使用重叠窗口。如果您想要重叠的用户数量,只需将其更改为重叠窗口.user_id (参见小提琴)。我还添加了另一列,使其看起来与您的结果集完全一样:) sqlfiddle.com/#!2/2aad9/16
【解决方案2】:

这样的事情应该让你开始 -

SELECT slots.time_slot, COUNT(*) AS num_users, GROUP_CONCAT(DISTINCT user_bookings.user_id ORDER BY user_bookings.user_id) AS user_list
FROM (
    SELECT CURRENT_DATE + INTERVAL ((id-1)*30) MINUTE AS time_slot
    FROM dummy
    WHERE id BETWEEN 1 AND 48
) AS slots
LEFT JOIN user_bookings
    ON slots.time_slot BETWEEN `user_bookings`.`start` AND `user_bookings`.`end`
GROUP BY slots.time_slot
ORDER BY num_users DESC

这个想法是创建一个由当天的时间段组成的派生表。在此示例中,我使用了 dummy(可以是任何具有与所需集合连续的 AI id 的表)通过增量添加 30 分钟来创建时隙列表。然后将此结果与预订相结合,以便能够计算每个时间段的书籍数量。

更新对于整个日期/时间范围,您可以使用这样的查询来获取所需的其他数据 -

SELECT MIN(`start`) AS `min_start`, MAX(`end`) AS `max_end`, DATEDIFF(MAX(`end`), MIN(`start`)) + 1 AS `num_days`
FROM user_bookings

然后可以将这些值替换到原始查询中,或者可以将两者结合起来 -

SELECT slots.time_slot, COUNT(*) AS num_users, GROUP_CONCAT(DISTINCT user_bookings.user_id ORDER BY user_bookings.user_id) AS user_list
FROM (
    SELECT DATE(tmp.min_start) + INTERVAL ((id-1)*30) MINUTE AS time_slot
    FROM dummy
    INNER JOIN (
        SELECT MIN(`start`) AS `min_start`, MAX(`end`) AS `max_end`, DATEDIFF(MAX(`end`), MIN(`start`)) + 1 AS `num_days`
        FROM user_bookings
    ) AS tmp
    WHERE dummy.id BETWEEN 1 AND (48 * tmp.num_days)
) AS slots
LEFT JOIN user_bookings
    ON slots.time_slot BETWEEN `user_bookings`.`start` AND `user_bookings`.`end`
GROUP BY slots.time_slot
ORDER BY num_users DESC

编辑我在GROUP_CONCAT() 中添加了DISTINCTORDER BY 子句以响应您的上一个查询。

请注意,您将需要虚拟表中更大范围的 id。我没有测试这个查询,所以它可能有语法错误。

【讨论】:

  • 所以我的默认表是 user_bookings 和 user_id,开始,结束。然后我用 id (AI) 创建了 table dummy。但是请问我还应该用table dummy做什么?我正在努力让它发挥作用。
  • 您不需要创建表dummy,尽管我的服务器上确实有一个表可以防止在 id 序列中留下漏洞。您可以使用任何具有覆盖所需范围的 id 的表。我的示例基于当天的 48 * 30 分钟时段,因此您只需要 1 到 48 之间的 id。
  • 谢谢。它涵盖 48*30,但仅适用于今天。我认为这是一个很好的开始,但不知道如何适应我的需要。
  • 在这种情况下,您必须更好地解释您的需求。我确实开始问 - “你想在当天/周/月/年/所有时间运行这个吗?”只要您有足够大的 id 范围可以传入,您就可以将用于创建时隙的范围扩大到您想要的任何值。
  • 是的,我想在“所有时间”运行它,或者更好地说,我可以从开始和日期的 db 最大值和最小值中找到,所以我想在这个日期时间范围内运行它。
【解决方案3】:

我不会在 SQL 方面做太多事情,这在编程语言中要简单得多,SQL 不是为这样的事情而设计的。

当然,将一天分解为“时间段”是明智的 - 这是统计数据。但是,一旦您开始处理 00:00 边界上的日期,当您使用连接和内部选择时,事情就会开始变得棘手。特别是对于不太喜欢内部选择的 MySQL。

这是一个可能的 SQL 查询

SELECT count(*) FROM `times`
WHERE
  ( DATEDIFF(`Start`,`End`) = 0 AND
    TIME(`Start`) < TIME('$SLOT_HIGH') AND
    TIME(`End`) > TIME('$SLOT_LOW'))
  OR
  ( DATEDIFF(`Start`,`End`) > 0 AND
    TIME(`Start`) < TIME('$SLOT_HIGH') OR
    TIME(`End`) > TIME('$SLOT_LOW')

这是一些伪代码

granularity = 30*60; // 30 minutes
numslots = 24*60*60 / granularity;
stats = CreateArray(numslots);
for i=0, i < numslots, i++ do
  stats[i] = GetCountFromSQL(i*granularity, (i+1)*granularity); // low, high
end

是的,这会产生numslots 查询,但没有连接就什么都没有,因此它应该很快。您还可以轻松更改分辨率。

另一个积极的事情是,你可以“问自己”,“我有两个可能的时间段,我需要一个人多的时间段,我应该使用哪个?”并且只需在各自的范围内运行两次查询,您就不会被预定义的时间段困住。

要仅查找完全重叠(条目仅在其覆盖整个插槽时才计入),您必须在查询中切换低和高范围。

您可能已经注意到,我不会在可能跨越多天的条目之间添加时间,但是,添加一整天只会将所有时隙增加一个,从而变得毫无用处。 但是,您可以通过选择 sum(DAY(End) - DAY(Start)) 来添加它们,并将返回值添加到所有插槽。

【讨论】:

    【解决方案4】:

    表格看起来很简单。我会让你的 SQL 查询非常简单:

    SELECT * FROM 表名

    然后,当您将信息保存在 PHP 对象中时。使用循环和比较使用 PHP 进行处理。

    最简单的形式:

    for($x, $numrows = mysql_num_rows($query); $x < $numrows; $x++){
    
         /*Grab a row*/
         $row = mysql_fetch_assoc($query);
    
         /*store userID, START, END*/
         $userID = $row['userID'];
         $start = $row['START'];
         $end = $row['END'];
    
         /*Have an array for each user in which you store start and end times*/  
    
         if(!strcmp($userID, "A")
         {
            /*Store info in array_a*/
         }
         else if(!strcmp($userID, "B")
         {
            /*etc......*/
         } 
    }
     /*Now you have an array for each user with their start/stop times*/
    
     /*Do your loops and comparisons to find common time slots. */
    
     /*Also, use strtotime() to switch date/time entries into comparable values*/
    

    当然,这是非常基本的形式。您可能希望对数组进行一次循环,以首先获取所有用户 ID,然后再在上面显示的循环中进行比较。

    【讨论】:

      猜你喜欢
      • 2018-03-03
      • 2017-06-08
      • 1970-01-01
      • 1970-01-01
      • 2023-01-19
      • 1970-01-01
      • 2011-02-02
      • 1970-01-01
      相关资源
      最近更新 更多