这对我的方法并不重要,但我假设您有一个 items 表。我还将提供一个不需要项目表的查询。单独的项目表的优点是您可以随着时间的推移轻松添加或淘汰项目。它们会自动显示在预订查询结果中,您可以稍后添加条件,如 WHERE retireDate IS NULL or retireDate > @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.item 为NOT 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
最后,使用Strawberry 的answer 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