【问题标题】:Find big enough gaps in booking table在预订表中找到足够大的空白
【发布时间】:2013-10-22 22:07:15
【问题描述】:

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

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,即 1 天。系统会告诉他,该项目在给定的时间范围内不可用,因为没有预订。 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 中实现,而无需遍历每个预订及其后继?

【问题讨论】:

  • “尽可能接近所需的开始日期。”这是否真的意味着“尽快在或之后所需的开始日期/时间?”
  • @OllieJones 这意味着尽快所需的日期/时间之后。
  • 预订系统通常具有定义的粒度,您的情况是这样吗? (预订时间是否必须精确到小时,或者可能允许分钟,但只能是 15 的倍数?12:00、12:15 等?)
  • @MatBailie 在我的情况下,预订时间总是准确的时间。所以最短租期是从 XX:00 到 YY:00 的一小时。

标签: mysql sql gaps-and-islands


【解决方案1】:

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

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

【讨论】:

    【解决方案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中,外部查询找到需要的行。

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

    编辑

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

    编辑

    SQLfiddle

    【讨论】:

    • 我会预先播种@lastenddate。 SELECT @lastenddate:=COALESCE(MAX(enddate), '2013-10-27 12:00:00') FROM bookings WHERE startdate &lt; '2013-10-27 12:00:00' 之类的东西,然后也在 WHERE 子句中使用它来仅从该点开始搜索。
    • 您已经有几个嵌套查询,我相信这会很好。只需更改init 并添加WHERE schedule.startdate &gt;= init.timestamp?类似的东西?
    • 这就是我所说的“您可以通过在内部查询中使用一些智能 WHERE 来减少迭代次数”
    【解决方案3】:

    在 SQL 中搜索重叠的日期范围通常会产生较差的性能。因此,拥有可用插槽的“日历”通常会使事情变得更有效率。

    例如,预订 2013-10-25 16:00 =&gt; 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
    

    【讨论】:

    • 有趣,但这意味着当新发现的预订可能性被客户实际接受并且您需要将INSERT 输入数据库时​​,您需要更改所有的remaining_duration 值在此新预订之前属于该预订的记录,对吗?
    【解决方案4】:

    好吧,这在 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
    

    【讨论】:

      【解决方案5】:

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

      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

      【讨论】:

      • 条件 s。您需要在您尝试预订的时间段之后和之前进行预订。可能是“愚蠢的标记”,在 2000 年 1 月 1 日和 2999 年 12 月 31 日预订了 0 小时
      • @MatBailie 你是对的,更新的查询在这种情况下也能正常工作
      【解决方案6】:

      如果我正确理解了您的要求,您可以尝试自行加入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 分钟的时间段可用。

      一个问题可能是,如果 no 个句点可用,则查询返回 nothing -- 然后您需要另一个查询来获取最远的 enddate。当然,您可以使用UNIONLIMIT 1 来执行此操作,但我认为最好只按需运行“恢复”查询,以编程方式(即if empty(query) then new_query...)。

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

      【讨论】:

        猜你喜欢
        • 2013-07-21
        • 2018-03-25
        • 2016-03-02
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2021-07-28
        • 2013-10-12
        • 2013-01-16
        相关资源
        最近更新 更多