在预订表中找到足够大的空白

时间:2013-10-22 22:07:16

标签: mysql sql gaps-and-islands

租赁系统使用预订表存储所有预订和预订:

booking | item | startdate        | enddate
1       | 42   | 2013-10-25 16:00 | 2013-10-27 12:00
2       | 42   | 2013-10-27 14:00 | 2013-10-28 18:00
3       | 42   | 2013-10-30 09:00 | 2013-11-01 09:00
…

假设用户想要从2013-10-27 12:00直到2013-10-28 12:00租用项目42,这是一天的时间段。系统将告诉他,该项目在给定的时间范围内不可用,因为预订号码。 2碰撞。

现在,我想建议所选项目再次可用的最早租赁日期和时间。当然,考虑用户所要求的期限(1天),从用户所需的日期和时间开始。

所以在上面的例子中,我正在寻找一个返回2013-10-28 18:00的SQL查询,自2013-10-27 12:00以来最早的日期,其中第42项可用于1当天,是2013-10-28 18:00至2013-10-29 18:00。

因此,我需要找到预订之间的差距,这个差距足以容纳用户的预订,并且与预期的开始日期尽可能接近。

或换句话说:我需要找到给定商品的第一个预订,之后有足够的空闲时间来预订用户。

这是否可以在普通SQL中进行,而不必遍历每个预订及其后继者?

6 个答案:

答案 0 :(得分:3)

如果您无法重新设计数据库以使用更高效的内容,那么这将得到答案。你显然想要参数化它。它说要找到所需的日期,或者最早的结束日期,即租用间隔与现有预订不重叠:

Select
    min(startdate)
From (
    select
        cast('2013-10-27 12:00' as datetime) startdate
    from
        dual
    union all
    select
        enddate
    from
        booking
    where
        enddate > cast('2013-10-27 12:00' as datetime) and
        item = 42
    ) b1
Where
    not exists (
        select 
             'x'
        from
            booking b2
        where
            item = 42 and
            b1.startdate < b2.enddate and
            b2.startdate < date_add(b1.startdate, interval 24 hour)
    );

<强> Example Fiddle

答案 1 :(得分:2)

SELECT startfree,secondsfree FROM (
  SELECT
    @lastenddate AS startfree,
    UNIX_TIMESTAMP(startdate)-UNIX_TIMESTAMP(@lastenddate) AS secondsfree,
    @lastenddate:=enddate AS ignoreme
  FROM
    (SELECT startdate,enddate FROM bookings WHERE item=42) AS schedule,
    (SELECT @lastenddate:=NOW()) AS init
  ORDER BY startdate
) AS baseview
WHERE startfree>='2013-10-27 12:00:00'
  AND secondsfree>=86400
ORDER BY startfree
LIMIT 1
;

一些解释:内部查询使用变量将迭代移动到SQL中,外部查询找到所需的行。

也就是说,如果DB结构与给定的结构相似,我不会在SQL中执行此操作。您可以通过在内部查询中使用一些smort WHERE来减少迭代次数,以达到理智的时间跨度,但很可能,这不会很好。

修改

警告:我没有检查,但我认为,如果列表中没有事先预约,这将不起作用 - 这应该不是问题,因为在这种情况下您的第一次预订尝试(原始时间)会工作的。

修改

SQLfiddle

答案 2 :(得分:1)

搜索重叠日期范围通常会导致SQL性能不佳。因此,拥有可用插槽的“日历”通常可以提高效率。

例如,预订2013-10-25 16:00 => 2013-10-27 12:00实际上将由44条记录代表,每条记录一小时。

在下一次预订2013-10-27 14:00之前的“差距”将由2条记录表示,每条记录一个小时。

然后,每个记录也可以具有持续时间(时间或插槽数),直到下一次更改。

 slot_start_time  | booking | item | remaining_duration
------------------+---------+------+--------------------
 2013-10-27 10:00 |    1    |  42  |      2
 2013-10-27 11:00 |    1    |  42  |      1
 2013-10-27 12:00 |  NULL   |  42  |      2
 2013-10-27 13:00 |  NULL   |  42  |      1
 2013-10-27 14:00 |    2    |  42  |     28
 2013-10-27 15:00 |    2    |  42  |     27
 ...              |  ...    | ...  |    ...
 2013-10-28 17:00 |    2    |  42  |      1
 2013-10-28 18:00 |  NULL   |  42  |     39
 2013-10-28 19:00 |  NULL   |  42  |     38

然后您的查询变为:

SELECT
  *
FROM
  slots
WHERE
  slot_start_time >= '2013-10-27 12:00'
  AND remaining_duration >= 24
  AND booking IS NULL
ORDER BY
  slot_start_time ASC
LIMIT
  1

答案 3 :(得分:1)

好的,这在MySQL中并不常见。那是因为我们必须在子查询中伪造rownum值。

基本方法是将预订表的适当子集加入到自身偏移一个。

这是按预订时间排序的第42项预订的基本清单。我们无法通过booking_id订购,因为这些都不能保证按预订时间顺序排列。 (您是否尝试在两个现有预留之间插入新预订,嗯?)http://sqlfiddle.com/#!2/62383/9/0

  SELECT @aserial := @aserial+1 AS rownum,
         booking.*
    FROM booking,
         (SELECT @aserial:= 0) AS q
   WHERE item = 42
   ORDER BY startdate, enddate

这个子集加入了自身。诀窍是a.rownum+1 = b.rownum,它将每一行连接到预订表子集中紧跟其后的那一行。 http://sqlfiddle.com/#!2/62383/8/0

SELECT a.booking_id, a.startdate asta, a.enddate aend, 
                     b.startdate bsta, b.enddate bend 
  FROM (
      SELECT @aserial := @aserial+1 AS rownum,
             booking.*
        FROM booking,
             (SELECT @aserial:= 0) AS q
       WHERE item = 42
       ORDER BY startdate, enddate
       ) AS a
  JOIN (
      SELECT @bserial := @bserial+1 AS rownum,
             booking.*
        FROM booking,
             (SELECT @bserial:= 0) AS q
       WHERE item = 42
       ORDER BY startdate, enddate
       ) AS b ON a.rownum+1 = b.rownum

这里再次显示每个预订(最后一个除外)和其后的小时数。 http://sqlfiddle.com/#!2/62383/15/0

SELECT a.booking_id, a.startdate, a.enddate, 
       TIMESTAMPDIFF(HOUR, a.enddate, b.startdate) gaphours 
  FROM (
      SELECT @aserial := @aserial+1 AS rownum,
             booking.*
        FROM booking,
             (SELECT @aserial:= 0) AS q
       WHERE item = 42
       ORDER BY startdate, enddate
       ) AS a
  JOIN (
      SELECT @bserial := @bserial+1 AS rownum,
             booking.*
        FROM booking,
             (SELECT @bserial:= 0) AS q
       WHERE item = 42
       ORDER BY startdate, enddate
       ) AS b ON a.rownum+1 = b.rownum

因此,如果您要查找最早的12小时广告位的开始时间和结束时间,则可以使用该结果集执行此操作:http://sqlfiddle.com/#!2/62383/18/0

SELECT MIN(enddate) startdate, MIN(enddate) + INTERVAL 12 HOUR as enddate
   FROM (
    SELECT a.booking_id, a.startdate, a.enddate, 
           TIMESTAMPDIFF(HOUR, a.enddate, b.startdate) gaphours 
      FROM (
          SELECT @aserial := @aserial+1 AS rownum,
                 booking.*
            FROM booking,
                 (SELECT @aserial:= 0) AS q
           WHERE item = 42
           ORDER BY startdate, enddate
           ) AS a
      JOIN (
          SELECT @bserial := @bserial+1 AS rownum,
                 booking.*
            FROM booking,
                 (SELECT @bserial:= 0) AS q
           WHERE item = 42
           ORDER BY startdate, enddate
           ) AS b ON a.rownum+1 = b.rownum
    ) AS gaps
    WHERE gaphours >= 12

答案 4 :(得分:1)

这是查询,它将返回所需的日期,明显的条件 - 表格中应该有一些预订,但正如我从问题中看到的 - 你做了这个检查:

SELECT min(enddate)
FROM
(
    select a.enddate from table4 as a
    where
        a.item=42 
    and
        DATE_ADD(a.enddate, INTERVAL 1 day) <= ifnull(
            (select min(b.startdate)
            from table4 as b where b.startdate>=a.enddate and a.item=b.item),
        a.enddate)
    and
        a.enddate>=now()
    union all
    select greatest(ifnull(max(enddate), now()),now()) from table4
) as q

您将更改INTERVAL 1 day更改为INTERVAL ### hour

答案 5 :(得分:1)

如果我已正确理解您的要求,您可以尝试自我加入book,以获得“空”空格,然后适合。这只是MySQL(我相信它可以适应其他人 - 当然是PostgreSQL):

SELECT book.*, TIMESTAMPDIFF(MINUTE, book.enddate, book.best) AS width FROM
(
    SELECT book.*, MIN(book1.startdate) AS best
    FROM book
    JOIN book AS book1 USING (item)
    WHERE item = 42 AND book1.startdate >= book.enddate
    GROUP BY book.booking
) AS book HAVING width > 110 ORDER BY startdate LIMIT 1;

在上面的例子中,“110”是寻找的最小宽度,以分钟为单位。

同样的事情,可读性稍差(对我来说),删除了一个SELECT(非常快的SELECT,所以没什么优势):

SELECT book.*, MIN(book1.startdate) AS best
  FROM book
  JOIN book AS book1 ON (book.item = book1.item AND book.item = 42)
WHERE book1.startdate >= book.enddate
  GROUP BY book.booking
  HAVING TIMESTAMPDIFF(MINUTE, book.enddate, best) > 110
  ORDER BY startdate LIMIT 1;

在你的情况下,一天是1440分钟

SELECT book.*, MIN(book1.startdate) AS best       FROM book       JOIN book AS book1 ON (book.item = book1.item AND book.item = 42)     WHERE book1.startdate >= book.enddate       GROUP BY book.booking       HAVING TIMESTAMPDIFF(MINUTE, book.enddate, best) >= 1440       ORDER BY startdate LIMIT 1;
+---------+------+---------------------+---------------------+---------------------+
| booking | item | startdate           | enddate             | best                |
+---------+------+---------------------+---------------------+---------------------+
|       2 |   42 | 2013-10-27 14:00:00 | 2013-10-28 18:00:00 | 2013-10-30 09:00:00 |
+---------+------+---------------------+---------------------+---------------------+
1 row in set (0.00 sec)

...返回的时间段为2,即在预订2结束时,直到预订3的“最佳”,至少有1440分钟的时间段。

问题可能是,如果没有期间可用,查询会返回 nothing - 那么您需要另一个查询来获取最远的enddate。您可以使用UNIONLIMIT 1来执行此操作,但我认为最好只按需编程运行“恢复”查询(即if empty(query) then new_query...)。

此外,在内部WHERE中,您应添加NOW()的检查,以避免过去的日期。如果过期预订被移至非活动存储,则可能没有必要。