MySQL/PHP:根据表中给定重叠的日期时间范围搜索行

MySQL/PHP:根据表中给定重叠的日期时间范围搜索行,php,mysql,Php,Mysql,我有一张表格,显示人们何时可以工作,如下所示: +------+---------------------+---------------------+ | name | start | end | +------+---------------------+---------------------+ | Odin | 2015-07-01 11:00:00 | 2015-07-01 11:30:00 | | Thor | 2

我有一张表格,显示人们何时可以工作,如下所示:

+------+---------------------+---------------------+
| name | start               | end                 |
+------+---------------------+---------------------+    
| Odin | 2015-07-01 11:00:00 | 2015-07-01 11:30:00 |
| Thor | 2015-07-01 11:00:00 | 2015-07-01 11:30:00 |
| Odin | 2015-07-01 11:20:00 | 2015-07-01 12:45:00 |
| Odin | 2015-07-01 12:30:00 | 2015-07-01 15:30:00 |
| Thor | 2015-07-01 15:00:00 | 2015-07-01 17:00:00 |
+------+---------------------+---------------------+
我想检查在给定范围内是否有特定的人可以工作。例如,我希望有一个PHP函数,返回给定范围内可供工作的人员的姓名,如:
canWork($start,$end)

这一重要部分是处理重叠,特别是因为表格可能非常非常大。例如,如果我调用了
canWork('2015-07-01 11:10:00','2015-07-01 15:30:00')
我希望得到
Odin
,因为表中的第一行、第三行和第四行确实涵盖了该范围


有没有一种简单的方法可以用MySQL实现这一点?还是PHP?

在这种大数据情况下,尽量避免数据循环。在一个类似的练习中,SQL能够在几秒钟内交付代码中花费数小时的内容。聪明地看一看数据是值得的

这里最明智的一步是:通过检查时间之和,可以减少可能的匹配数:范围内的时间应等于(或小于)记录中的时间之和

但是,由于输入的开始时间可能小于您要查找的开始时间,而结束时间可能大于您要查找的结束时间,因此您首先必须找到最接近结束时间的结束时间和最接近开始时间的开始时间

(end是一个保留字,因此此代码不适用于该columnname,endtime和starttime是计划检查的变量)

每个用户的开始时间(最后可能):

将这些元素结合在一起,可以得到可能用户的子集,并且可以在

SELECT name, MAX_start,MIN_end
FROM 
(SELECT name,MIN(`end`) AS MIN_end
FROM scheduleTable
WHERE `end`>=endtime
GROUP BY name) a
INNER JOIN
(SELECT name,MAX(start) AS MAX_start
FROM scheduleTable
WHERE start<=starttime
GROUP BY name) b ON a.name=b.name;
您可能需要将开始时间和结束时间转换为unix时间,或者使用mysql日期时间函数进行计算

可能仍然存在差距:这些差距需要再次检查。为此,请使用group_concat获取一些数据,我们可以将其作为一个调用传递到函数中。函数结果为0,表示未找到间隙,1表示已找到间隙:

SELECT a.name
FROM (
  SELECT st.name,
    GROUP_CONCAT(start ORDER BY start ASC SEPARATOR ',') starttimelist,
    GROUP_CONCAT(`end` ORDER BY `end` ASC SEPARATOR ',') endtimelist
  FROM scheduleTable st
  INNER JOIN (
    SELECT name, MAX_start AS start,MIN_end AS end
    FROM 
    (SELECT name,MIN(`end`) AS MIN_end
    FROM scheduleTable
    WHERE `end`>=endtime
    GROUP BY name) a
    INNER JOIN
    (SELECT name,MAX(start) AS MAX_start
    FROM scheduleTable
    WHERE start<=starttime
    GROUP BY name) b ON a.name=b.name
  ) et ON st.name=et.name
  WHERE et.start>={starttime} AND `end`<=et.endtime AND et.name=st.name
  GROUP BY st.name
  HAVING SUM(st.`end`-st.start)>=(endtime-starttime);
) a
WHERE gapCheck(starttimelist,endtimelist)=0;

我认为最短的方法是

1) 首先,将同一个人的所有时间线合并为重叠。 例如,将第1行和第3行合并,将第1行的结束时间更改为“2015-07-01 12:45:00”(第3行将被删除或标记为已使用),然后将第1行和第4行合并,再次将第1行的结束时间更改为“2015-07-01 15:30:00”

2) 一旦有了一个不重叠的时间线表,就很容易找到start=$end所在的行

对于1)我更喜欢在PHP中执行此过程,首先在数据结构中复制整个表

$a = array();
//in a for loop after a select all query: for (all elements) {
  $a[$name][$start] = $end));
//} end of for loop
然后删除该数据结构中的所有重叠:

for($a as $currName => $timeArray) {
  ksort($timeArray);
  removeOverlaps(&$timeArray);
}

function removeOverlaps($timeArray) {
  $allKeys = array_keys($timeArray);
  $arrLength = count($allKeys);
  for ($i = 0; $i < $arrlength; ++$i) {
    $start = $allKeys[$i];
    if(array_key_exists($start, $timearray)) {
      $end = $timeArray[$start])
      for ($j = $i; $j < $arrlength; ++$j) {
        $newStart = $allKeys[$j];
        $newEnd = $timeArray[$newStart];
        if($newStart <= $end) && ($newEnd > $end)) {
          $timeArray[$start] = $newEnd;
          unset($timeArray[$newStart]);
        }
      }
    }
  }
}
for($a作为$currName=>$timeArray){
ksort($timeArray);
移除重叠(&$timeArray);
}
函数removeOverlaps($timeArray){
$allKeys=array\u keys($timeArray);
$arrLength=计数($allkey);
对于($i=0;$i<$arrlength;++$i){
$start=$allkey[$i];
如果(数组\键\存在($start,$timearray)){
$end=$timeArray[$start])
对于($j=$i;$j<$arrlength;++$j){
$newStart=$allKeys[$j];
$newEnd=$timeArray[$newStart];
如果($newStart$end)){
$timeArray[$start]=$newEnd;
未设置($timeArray[$newStart]);
}
}
}
}
}

然后继续2)。

为什么Odin的可用时间重叠?为什么一号线的终点在三号线的起点之后?这绝对是个棘手的问题。获取时间范围内的行很容易,但是您需要确保在可用性方面没有差距。@Barmar A God canmultitask@Barmar设计选择。人们可以输入自己的班次以提供最大的灵活性,我们只需存储并在以后进行调整。这是其中一项调整。
SELECT a.name
FROM (
  SELECT st.name,
    GROUP_CONCAT(start ORDER BY start ASC SEPARATOR ',') starttimelist,
    GROUP_CONCAT(`end` ORDER BY `end` ASC SEPARATOR ',') endtimelist
  FROM scheduleTable st
  INNER JOIN (
    SELECT name, MAX_start AS start,MIN_end AS end
    FROM 
    (SELECT name,MIN(`end`) AS MIN_end
    FROM scheduleTable
    WHERE `end`>=endtime
    GROUP BY name) a
    INNER JOIN
    (SELECT name,MAX(start) AS MAX_start
    FROM scheduleTable
    WHERE start<=starttime
    GROUP BY name) b ON a.name=b.name
  ) et ON st.name=et.name
  WHERE et.start>={starttime} AND `end`<=et.endtime AND et.name=st.name
  GROUP BY st.name
  HAVING SUM(st.`end`-st.start)>=(endtime-starttime);
) a
WHERE gapCheck(starttimelist,endtimelist)=0;
CREATE FUNCTION gapCheck(IN starttimeList VARCHAR(200),endtimeList VARCHAR(200))
BEGIN
  DECLARE helperTimeStart,helperTimeEnd,prevHelperTimeStart,prevHelperTimeEnd DATETIME
  DECLARE c,splitIndex,gap INT
SET c-0;
SET gap=0;
WHILE(c=0) DO
  SET splitIndex=INSTR(starttimeList,',');
  IF(splitIndex>0) THEN
    SET helperTimeStart=SUBSTRING(starttimeList,1,splitIndex-1);
    SET starttimeList=SUBSTRING(starttimeList,splitIndex); /* String for the next iteration */
  ELSE
    SET helperTimeStart=starttimeList; /* End of list reached */
    SET helperTimeEnd=endtimeList; /* end can be set too: Lists are of same length */
    SET c=1;
  END IF;

  IF(splitIndex>0) THEN
    SET splitIndex=INSTR(endtimeList,',');
    SET helperTimeEnd=SUBSTRING(endtimeList,1,splitIndex-1);
  END IF;

  IF prevHelperTimeEnd>=helperTimeEnd THEN /* if prevHelperTimeEnd is not set, this is false and the check is skipped: on the first record we can not check anything */
    /* If previous end time > current start time: We have a gap */
    IF CAST(prevHelperTimeEnd AS DATETIME)>=CAST(helperTimeStart AS DATETIME) THEN
      gap=1;
    END IF;
  END IF;

  /* save some data for the next loop */
  SET prevHelperTimeStart=helperTimeStart;
  SET prevHelperTimeEnd=helperTimeEnd;
END WHILE;
RETURN gap;
END;
$a = array();
//in a for loop after a select all query: for (all elements) {
  $a[$name][$start] = $end));
//} end of for loop
for($a as $currName => $timeArray) {
  ksort($timeArray);
  removeOverlaps(&$timeArray);
}

function removeOverlaps($timeArray) {
  $allKeys = array_keys($timeArray);
  $arrLength = count($allKeys);
  for ($i = 0; $i < $arrlength; ++$i) {
    $start = $allKeys[$i];
    if(array_key_exists($start, $timearray)) {
      $end = $timeArray[$start])
      for ($j = $i; $j < $arrlength; ++$j) {
        $newStart = $allKeys[$j];
        $newEnd = $timeArray[$newStart];
        if($newStart <= $end) && ($newEnd > $end)) {
          $timeArray[$start] = $newEnd;
          unset($timeArray[$newStart]);
        }
      }
    }
  }
}