这对我的方法并不重要,但我假设你有一张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
)
请注意,如果您没有单独的 items 表,则可以替换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
. 我们忽略了查询中使用的事实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对有关在 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