【问题标题】:The core of a reservation system - finding an unreserved item efficiently预订系统的核心 - 有效地找到未预订的项目
【发布时间】:2013-04-11 01:44:02
【问题描述】:

这似乎是一个常见问题,但我一直在网上搜索并找不到答案。

我想保留几天的东西(没有部分天),所以我想我需要一张类似的桌子:

CREATE TABLE reservations 
    (
     item int, 
     customer int, 
     startDate date, 
     endDate date
    );

(嗯,我的主键是什么?item 和 startDate?我还需要 PK 吗?)

但我的主要问题是如何在给定开始和结束日期的情况下找到免费物品。我的SELECT ... 是什么样的?

对于奖励分数,我们是否可以假设所有项目都是相同的,并且我希望尽可能提高效率,所以如果我想从星期五开始预订,我希望找到一个保留到星期四的项目(并且,因此从星期五开始免费)。

对于双倍奖励分数,如果我需要 X 天的商品,我想在尽可能接近 X 天时找到预订有漏洞的商品。

我认为问题在于我试图找到不存在的东西(现有预订)。我发现的所有其他解决方案似乎都有一个带有项目 ID 的可预订日期表(值为 NULL、0 或 -1 表示“尚未预订”)。这对我来说似乎效率低下。这张表会延伸到多远的未来?

注意:有些人在询问读取与写入的比率。显然,每次预订只进行一次,所以这是一次写入(可能每天一次,具体取决于实现),当用户搜索未预订的插槽时,我预计会多次读取。

【问题讨论】:

  • 我很惊讶您根本没有对我的回答发表评论,因为我相信它通过一个查询就可以满足您的所有需求。它也不需要任何额外的工作,例如跟踪未保留的时间段。只是好奇,因为我确实花了一些时间 - 有什么我没有涵盖的吗?

标签: mysql


【解决方案1】:
SELECT item FROM reservations WHERE 
(endDate BETWEEN start AND end) OR (startDate BETWEEN start AND end) OR (startDate<start AND endDate>end)

@Strawberry 建议更好的查询如下所示

SELECT item FROM reservations WHERE
start<endDate AND end>startDate

这将为您提供在您正在寻找的日期拍摄的物品。 现在您需要查找不在此列表中的项目。所以如果你有一张桌子,里面有物品,你可以这样写

SELECT * FROM items WHERE item NOT IN 
SELECT item FROM reservations WHERE
start<endDate AND end>startDate)

你会得到在你搜索期间免费的项目。

start,end 是日期你查找 startDate,endDate 是列。

SELECT item, start-r.startDate as diff FROM items as i 
LEFT JOIN reservations as r USING(item) 
WHERE i.item NOT IN 
(SELECT item FROM reservations WHERE
start<endDate AND end>startDate
) ORDER BY diff

没有模式来测试它,但这个查询应该是你的第一个奖励的答案

至于第二个,这需要在一张表的行之间做一些数学运算,现在我不知道如果可能的话如何在纯 MySQL 中做。

//编辑

当现有预订在搜索期之前和之后结束时,我用另一个条件更新了查询。

对于第二个奖励问题,这应该可以工作

SELECT item, r1.startDate-r2.endDate as diff FROM reservations as r1 JOIN (SELECT * FROM reservations) as r2 USING (item)
WHERE r1.startDate-r2endDate>=x AND item NOT IN
(SELECT item FROM reservations WHERE
r1.startDate<endDate AND r2.endDate>startDate)
ORDER BY diff ASC

但这将是非常昂贵的查询。可能需要从子查询中的日期中加/减一天。

正如您在所有这些中看到的那样,我从帖子开头使用查询作为子查询,对于第一个和第二个查询,这不会是一个大问题,因为它只会执行一次。在对第二个奖励的最后一个查询中,它必须分别为每一行执行(因为每个项目都有一个连接,给定项目的保留数量是 2 的幂),这可能是一个瓶颈。

我不知道您要保留的那些项目是什么,但如果它们不是很多

【讨论】:

  • 不知道有没有更简单的方式来表达这个概念?
  • 这个很简单。我想知道是否有更复杂但更优化的方法,但我没有想到。我说的是奖金问题,因为第一个是你能得到的最好的。您可以通过否定条件而不是将其放入子查询中来稍微改进它,但您不会得到太多。
  • 嗯,不妨看看我前几天的回复……stackoverflow.com/questions/15521175/…
  • 是的,你看起来好多了,所以我错了说我拥有的是最好的 ^^
  • 我认为确实如此。如果没有,你能举个例子吗?
【解决方案2】:

这对我的方法并不重要,但我假设您有一个 items 表。我还将提供一个不需要项目表的查询。单独的项目表的优点是您可以随着时间的推移轻松添加或淘汰项目。它们会自动显示在预订查询结果中,您可以稍后添加条件,如 WHERE retireDate IS NULL or retireDate &gt; @reservationWindowEnd 以排除已停用的项目(而不是添加虚拟预订来实现相同的目标)。

举个例子,

CREATE TABLE items (
    item int, 
    description varchar(255),
    purchaseDate date,
    retireDate date
);

我们还为要匹配的预订窗口设置一些示例值。

mysql> set @newReservationStart='2013-06-01';
Query OK, 0 rows affected (0.00 sec)

mysql> set @newReservationEnd='2013-06-04';
Query OK, 0 rows affected (0.00 sec)

现在让我们找出在目标持续时间的至少一部分内保留的项目列表:

SELECT
    DISTINCT item
FROM reservations
WHERE
    @newReservationStart BETWEEN startDate AND endDate
    OR startDate BETWEEN @newReservationStart and @newReservationEnd

我们想要没有反转的项目列表,所以我们找到一个不在此列表中的项目列表:

SELECT
    item
FROM
    items
WHERE
    item NOT IN (
        SELECT
            DISTINCT item
        FROM reservations
        WHERE
            @newReservationStart BETWEEN startDate AND endDate
            OR startDate BETWEEN @newReservationStart and @newReservationEnd
    )

请注意,如果您没有单独的项目表,则可以将 SELECT item FROM items 替换为 SELECT DISTINCT item FROM reservations

现在我们有了一个已知可用项目的列表,让我们决定我们想要哪一个。

对于每个项目,我们需要知道它的哪些保留是在目标窗口之前最后结束的:

SELECT item, MAX(endDate) AS endDate
FROM reservations
WHERE endDate < @newReservationStart
GROUP BY item

我们会想知道它的哪个预订是在目标预订期之后最先开始的:

SELECT item, MIN(startDate) AS startDate
FROM reservations
WHERE @newReservationEnd < startDate
GROUP BY item

在进一步讨论之前,让我们将其放在一起以一次获取相关项目的所有这些信息:

SELECT
    items.item AS item,
    priorReservation.endDate AS priorEnd,
    nextReservation.startDate AS nextStart
FROM
    items
    LEFT JOIN
        (
            SELECT item, MAX(endDate) AS endDate
            FROM reservations
            WHERE endDate < @newReservationStart
            GROUP BY item
        ) priorReservation ON priorReservation.item = items.item
    LEFT JOIN
        (
            SELECT item, MIN(startDate) AS startDate
            FROM reservations
            WHERE @newReservationEnd < startDate
            GROUP BY item
        ) nextReservation ON nextReservation.item = items.item
WHERE
    items.item NOT IN (
        SELECT
            DISTINCT item
        FROM reservations
        WHERE
            @newReservationStart BETWEEN startDate AND endDate
            OR startDate BETWEEN @newReservationStart and @newReservationEnd
    )

不太破旧。我们还知道上一个预订何时结束以及下一个预订何时开始。如果没有先前或下一个预留,则 LEFT JOIN 确保相应的值为空。由于我们知道所有列出的项目都是可用的,因此我们可以根据需要进行排序。

我们可以通过最“舒适”的窗口订购:

ORDER BY DATEDIFF(nextStart, priorEnd)

或者最小化上一次预订结束和这次预订开始之间的时间:

ORDER BY DATEDIFF(@newReservationStart, priorEnd)

或者更喜欢从未被保留的新项目:

ORDER BY ISNULL(priorEnd) DESC

或者我们可以组合多个选项,偏爱新项目,然后选择最接近预订窗口开始日期返回的项目,然后选择可用性最符合目标窗口的项目:

ORDER BY
    ISNULL(priorEnd) DESC,
    DATEDIFF(nextStart, priorEnd),
    DATEDIFF(nextStart, priorEnd)

LIMIT 关键字甚至可以用来选择最合适的。综上所述,

SELECT
    items.item AS item,
    priorReservation.endDate AS priorEnd,
    nextReservation.startDate AS nextStart
FROM
    items
    LEFT JOIN
        (
            SELECT item, MAX(endDate) AS endDate
            FROM reservations
            WHERE endDate < @newReservationStart
            GROUP BY item
        ) priorReservation ON priorReservation.item = items.item
    LEFT JOIN
        (
            SELECT item, MIN(startDate) AS startDate
            FROM reservations
            WHERE @newReservationEnd < startDate
            GROUP BY item
        ) nextReservation ON nextReservation.item = items.item
WHERE
    items.item NOT IN (
        SELECT
            DISTINCT item
        FROM reservations
        WHERE
            @newReservationStart BETWEEN startDate AND endDate
            OR startDate BETWEEN @newReservationStart and @newReservationEnd
    )
ORDER BY
    ISNULL(priorEnd) DESC,
    DATEDIFF(nextStart, priorEnd),
    DATEDIFF(nextStart, priorEnd)
LIMIT 1

在合理的数据集上运行查询花费的时间长得令人失望。使用包含 155 个项目的示例数据集,每个项目大约有 30 个预订,大约需要 15 秒,这对于交互式应用程序来说太慢了。

MySQL 从“外向内”评估查询,使用最外层查询过滤传递给内部查询的行。因此,让我们将最外层的WHERE 子句放在“测试工具”查询中,看看EXPLAIN 揭示了什么。

mysql>解释 -> 选择 -> items.item -> 从 -> 项目 -> 在哪里 -> items.item 不在( -> 选择 -> 不同的项目 -> 从预订 -> 在哪里 -> @newReservationStart 在 startDate 和 endDate 之间 -> OR startDate BETWEEN @newReservationStart 和 @newReservationEnd ->) ->; +----+--------+--------------+------+- --------------+------+---------+------+------+---- --------------------------+ |编号 |选择类型 |表|类型 |可能的键 |关键 | key_len |参考 |行 |额外 | +----+--------+--------------+------+- --------------+------+---------+------+------+---- --------------------------+ | 1 |初级 |项目 |全部 |空 |空 |空 |空 | 155 |使用位置 | | 2 |依赖子查询 |预订 |全部 |空 |空 |空 |空 | 3871 |使用哪里;使用临时 | +----+--------+--------------+------+- --------------+------+---------+------+------+---- --------------------------+ 2 行(0.00 秒)

看起来不太好。 MySQL 正在为 items 表中的每一行运行子选择(“从属子查询”)。每次运行内部查询时,它都会查看reservations 表中的每个条目。 (这令人失望,因为内部查询产生的不同项目集实际上并不依赖于外部查询中 item 的值。但这就是 MySQL 的工作方式,Oracle DBA 最近的评论给了我印象中这种行为并不孤单。)

根据可用项目的总数,内部查询可能会运行多次。在我对 155 个项目的测试中,其中大多数每个都有大约 30 个现有预订,运行此查询大约需要 0.7 秒。

让我们尝试一个索引以避免对每个可用项目在reservations 上进行全表扫描。直观地说,我们可以从索引日期列开始。我们不在乎最终得到哪个项目,但我们非常有兴趣查看正确的时间段:

mysql> 创建索引 idx_startDate_endDate_item -> ON 预订(开始日期、结束日期、项目); 查询正常,0 行受影响(0.03 秒) 记录:0 重复:0 警告:0

不幸的是,这不会像预期的那样有帮助。 MySQL 对startDate BETWEEN @newReservationStart and @newReservationEnd 做得很好,因为它知道startDate 只能在一个狭窄的值范围内。 但是对于@newReservationStart BETWEEN startDate and endDate,我们不是在搜索可以缩小到一个小范围的单个列。 MySQL 必须找到在@newReservationStart 之前开始的所有预留,并决定哪些在@newReservationStart 之后结束。

运行相同的 EXPLAIN 语句,我们得到:

+----+--------+--------------+-------+ ----------------------------------------+---------- --------+---------+------+------+------ -------------------------------------+ |编号 |选择类型 |表|类型 |可能的键 |关键 | key_len |参考 |行 |额外 | +----+--------+--------------+-------+ ----------------------------------------+---------- --------+---------+------+------+------ -------------------------------------+ | 1 |初级 |项目 |全部 |空 |空 |空 |空 | 155 |使用位置 | | 2 |依赖子查询 |预订 |范围 | idx_startDate_endDate_item | idx_startDate_endDate_item | 4 |空 | 3572 |使用哪里;使用索引;使用临时 | +----+--------+--------------+-------+ ----------------------------------------+---------- --------+---------+------+------+------ -------------------------------------+

尽管有索引,但我们只查看了 3871 行到 3572 行。我们正在为 items.item 的每个值执行此操作。如果我们假设大多数预订都是过去的,我们可以通过索引(endDate、startDate、item)做得更好。这将从查看​​ endDate 在@newReservationStart 之后的项目开始,并且可能是一个较小的子集。但它仍然不理想。我们需要一个单独的索引,将startDate 作为第一列,因为OR 子句的另一部分会查找特定的开始日期范围。

那么现在呢?

我们知道 MySQL 将为 items.item 的每个值运行内部查询。所以我们真的只需要寻找我们当前正在检查的项目的预订。这可能意味着将查询转换为 SQL 连接,但让我们再试一次优化器。

mysql> ALTER TABLE 保留 DROP INDEX idx_startDate_endDate_item; 查询正常,0 行受影响(0.01 秒) 记录:0 重复:0 警告:0 mysql> 创建索引 idx_item_startDate -> ON 预订(项目,开始日期); 查询正常,0 行受影响(0.02 秒) 记录:0 重复:0 警告:0

再次运行 EXPLAIN 语句,我们得到

+----+--------+--------------+-------- --------+--------------------------------+-------- +---------+------+------+------------- ------------+ |编号 |选择类型 |表|类型 |可能的键 |关键 | key_len |参考 |行 |额外 | +----+--------+--------------+-------- --------+--------------------------------+-------- +---------+------+------+------------- ------------+ | 1 |初级 |项目 |全部 |空 |空 |空 |空 | 155 |使用位置 | | 2 |依赖子查询 |预订 |索引子查询 | idx_item_startDate | idx_item_startDate | 5 |功能 | 38 |使用哪里;对 NULL 键进行全扫描 | +----+--------+--------------+-------- --------+--------------------------------+-------- +---------+------+------+------------- ------------+

一点也不差!只是为了好玩,我们不妨通过创建items.itemNOT NULL 来消除“对NULL 键进行全扫描”注释。而且我们忽略了endDate在查询中使用,但它不在索引中的事实。 MySQL 将使用索引来完成大部分工作。没有理由让它查询完整的表来检查 endDate,所以让我们也替换索引:

mysql> ALTER TABLE items MODIFY item INT NOT NULL; 查询正常,155 行受影响(0.00 秒) 记录:155 重复:0 警告:0 mysql> ALTER TABLE 保留 DROP INDEX idx_item_startDate; 查询正常,0 行受影响(0.00 秒) 记录:0 重复:0 警告:0 mysql> CREATE INDEX idx_item_startDate_endDate ON 保留(item, startDate, endDate); 查询正常,0 行受影响(0.02 秒) 记录:0 重复:0 警告:0

EXPLAIN 现在给了我们:

+----+--------+--------------+-------- --------+----------------+------------ ----------------+---------+------+------+--------- -----------------+ |编号 |选择类型 |表|类型 |可能的键 |关键 | key_len |参考 |行 |额外 | +----+--------+--------------+-------- --------+----------------+------------ ----------------+---------+------+------+--------- -----------------+ | 1 |初级 |项目 |全部 |空 |空 |空 |空 | 155 |使用位置 | | 2 |依赖子查询 |预订 |索引子查询 | idx_item_startDate_endDate | idx_item_startDate_endDate | 5 |功能 | 38 |使用索引;使用位置 | +----+--------+--------------+-------- --------+----------------+------------ ----------------+---------+------+------+--------- -----------------+

MySQL 现在使用索引来获取来自reservations 的所有信息。查询运行时间为 0.14 秒,这对于交互式应用程序来说似乎是合理的。

如果您不想要单独的项目表,您可以执行以下操作。

SELECT
    reservationItems.item AS item,
    priorReservation.endDate AS priorEnd,
    nextReservation.startDate AS nextStart
FROM
    (SELECT DISTINCT item FROM reservations) AS reservationItems
    LEFT JOIN
        (
            SELECT item, MAX(endDate) AS endDate
            FROM reservations
            WHERE endDate < @newReservationStart
            GROUP BY item
        ) priorReservation ON priorReservation.item = reservationItems.item
    LEFT JOIN
        (
            SELECT item, MIN(startDate) AS startDate
            FROM reservations
            WHERE @newReservationEnd < startDate
            GROUP BY item
        ) nextReservation ON nextReservation.item = reservationItems.item
WHERE
    reservationItems.item NOT IN (
        SELECT
            DISTINCT item
        FROM reservations
        WHERE
            @newReservationStart BETWEEN startDate AND endDate
            OR startDate BETWEEN @newReservationStart and @newReservationEnd
    )
ORDER BY
    ISNULL(priorEnd) DESC,
    DATEDIFF(nextStart, priorEnd),
    DATEDIFF(nextStart, priorEnd)
LIMIT 1

最后,使用Strawberryanswer from a question about matching date ranges in SQL 将运行时间比我最初的方法减少了大约一半。有趣的是,EXPLAIN 的输出完全相同。但最终查询(如下所示)现在运行时间为 0.07 秒。

SELECT
    items.item AS item,
    priorReservation.endDate AS priorEnd,
    nextReservation.startDate AS nextStart
FROM
    items
    LEFT JOIN
        (
            SELECT item, MAX(endDate) AS endDate
            FROM reservations
            WHERE endDate < @newReservationStart
            GROUP BY item
        ) priorReservation ON priorReservation.item = items.item
    LEFT JOIN
        (
            SELECT item, MIN(startDate) AS startDate
            FROM reservations
            WHERE @newReservationEnd < startDate
            GROUP BY item
        ) nextReservation ON nextReservation.item = items.item
WHERE
    items.item NOT IN (
        SELECT
            DISTINCT item
        FROM reservations
        WHERE
            @newReservationStart <= endDate
            AND startDate <= @newReservationEnd
    )
ORDER BY
    ISNULL(priorEnd) DESC,
    DATEDIFF(nextStart, priorEnd),
    DATEDIFF(nextStart, priorEnd)
LIMIT 1

【讨论】:

  • 如果 EXPLAIN 相同,那么查询肯定(逻辑上)相同!?!? NOT IN 与 NOT EXISTS 相比如何?
  • 不一定;这意味着语句根据EXPLAIN 显示的指标匹配。优化器将丢弃它可以决定不相关的内容,但 MySQL 必须稍后再查看优化器没有拒绝的内容。
  • NOT EXISTS 似乎没有什么不同。我希望 MySQL 能够认识到即使是单个匹配也会导致 NOT IN 变为错误。
【解决方案3】:

这在数据库之外很容易做到 - 选择您愿意考虑进行预订的时间段内的所有预订,使用结果填充一个数组,其中一天为 1(已填充)或0(未填充)并扫描阵列以查找所需大小的间隙。 O(n) 但是一年只有 365 天,所以不会很慢。

【讨论】:

  • 如果有人想为未来 10 年的生日预订?
  • @Mawg 不要过早优化。对其进行编码 - 如果它很慢,则对其进行优化。
  • +1 很好,但我确实担心我应该在未来多长时间内扩展我的表格。如果需要,我想我可以在应用程序中动态扩展它们,但这似乎很恶心。
  • @Mawg 如果您的数据库结构需要更改,那么您的程序的安装程序/升级程序可以像修改程序员一样修改数据库。这并不比发布新版本的程序更重要。
【解决方案4】:

正如其他人指出的那样,您可能不需要 这太高效了。也就是说,这是一种方法(取决于您的读写比率):

一种方法是使用一张表格来跟踪未预订的时间段(在您感兴趣的任何时间范围内,例如从 2000 年到 2020 年)。最初,每个项目都有一段空闲时间。 (我将以一种可读的方式列出这个;我将把架构留给你想象。)

FREE SLOTS
Item 1: January 1, 2000 - December 31, 2020
Item 2: January 1, 2000 - December 31, 2020

RESERVATIONS
(none)

当有人进行预订时,您创建一个预订,并将空闲位置分成两个较小的空闲位置(除非这会使它变空)。在此操作期间注意数据存储的锁定情况!

FREE SLOTS
Item 1: January 1, 2000 - May 4, 2012
Item 1: May 8, 2012 - December 31, 2020
Item 2: January 1, 2000 - December 31, 2020

RESERVATIONS
Item 1: May 5, 2012 - May 7, 2012, Barack Obama

删除预订后,您会立即检查前后的空闲时段。如果两者都存在,则将两个空闲槽和预留合并到一个空闲槽中。如果只存在一个,则将其扩展以填充先前预留所占用的空间。

由于您可以轻松地在表格中保留空闲插槽的持续时间,因此您可以轻松找到所需持续时间的插槽(确切地说,大于某个数量,在某个范围内等)。您支付的成本是在修改数据存储时确保一致性所需的锁定。

【讨论】:

    【解决方案5】:

    如果你能省下钱,Joe Celko's SQL for Smarties 可能有你需要的东西。

    【讨论】:

      【解决方案6】:

      如果查找效率是最重要的,那么您最好使用更像...的模式

      CREATE TABLE items
      (
          id          INT             NOT NULL    AUTO_INCREMENT,
          name        VARCHAR(255)    NOT NULL,
          PRIMARY KEY (id)
      );
      
      CREATE TABLE reservations 
      (
          item_id     INT     NOT NULL, 
          customer_id INT     NOT NULL, 
          reserved_on DATE    NOT NULL,
          PRIMARY KEY (item_id, reserved_on)
      );
      

      ...并为每个保留项目的日期添加单独的行。

      这样,数据库将确保您不能在同一日期多次预订同一项目,并且查找哪些项目 ID 是免费的,例如,2013-04-18 变成...

      SELECT
          i.id
      FROM items i
          LEFT JOIN reservations r ON (r.item_id=i.id AND r.reserved_on='2013-04-18')
      WHERE item_id IS NULL;
      

      ...EXPLAIN 显示的内容只需使用索引即可满足...

      +----+-------------+-------+--------+---------------+---------+---------+-----------------+------+--------------------------------------+
      | id | select_type | table | type   | possible_keys | key     | key_len | ref             | rows | Extra                                |
      +----+-------------+-------+--------+---------------+---------+---------+-----------------+------+--------------------------------------+
      |  1 | SIMPLE      | i     | index  | NULL          | PRIMARY | 4       | NULL            |   10 | Using index                          |
      |  1 | SIMPLE      | r     | eq_ref | PRIMARY       | PRIMARY | 7       | test.i.id,const |    1 | Using where; Using index; Not exists |
      +----+-------------+-------+--------+---------------+---------+---------+-----------------+------+--------------------------------------+
      

      这意味着在添加/修改保留时需要做更多的工作,但假设您将执行更多的读取操作而不是写入操作,这可能不会产生很大的开销。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2018-08-18
        • 2020-10-03
        • 2012-05-05
        • 2012-03-27
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多