私のアプローチにとって重要ではありませんが、items
テーブルがあると仮定します。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 により、対応する値が null になることが保証されます。リストされたすべてのアイテムが利用可能であることがわかっているので、基準を満たすように並べ替えることができます。
最も「ぴったり」のウィンドウで注文できます。
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
妥当なデータ セットに対してクエリを実行すると、残念なほど時間がかかります。約 30 件の予約を含む 155 項目のサンプル データ セットを使用すると、約 15 秒かかりました。これは、対話型アプリケーションには遅すぎます。
MySQL は、最も外側のクエリを使用して、内側のクエリに渡される行をフィルタリングし、「外側から」クエリを評価します。WHERE
では、最も外側の句を「テスト ハーネス」クエリに入れて、何EXPLAIN
が明らかになるか見てみましょう。
mysql> 説明する
-> 選択
-> アイテム.アイテム
-> から
-> アイテム
->どこで
-> items.item NOT IN (
-> 選択
-> DISTINCT アイテム
-> 予約から
->どこで
-> @newReservationStart startDate と endDate の間
-> または @newReservationStart と @newReservationEnd の間の startDate
-> )
-> ;
+----+--------------------+--------------+------+- --------------+------+---------+------+------+---- --------------------------+
| | ID | select_type | テーブル | タイプ | 可能な_キー | キー | key_len | 参照 | 行 | 行 エクストラ |
+----+--------------------+--------------+------+- --------------+------+---------+------+------+---- --------------------------+
| | 1 | プライマリ | アイテム | すべて | ヌル | ヌル | ヌル | ヌル | 155 | where | の使用
| | 2 | 従属サブクエリ | 予約 | すべて | ヌル | ヌル | ヌル | ヌル | 3871 | where を使用します。一時的な使用 |
+----+--------------------+--------------+------+- --------------+------+---------+------+------+---- --------------------------+
2 行セット (0.00 秒)
それはよく見えません。MySQL は、アイテム テーブルの各行に対してサブセレクト (「依存サブクエリ」) を実行しています。内部クエリを実行するたびに、reservations
テーブル内のすべてのエントリが調べられます。(これは残念なことです。内側のクエリによって生成された個別のアイテムのセットは、実際には外側のクエリの値に依存しないitem
からです。しかし、これが MySQL のしくみであり、Oracle DBA からの最近のコメントは、それがこの振る舞いは一人ではありません。)
使用可能なアイテムの総数によっては、内部クエリが何度も実行される可能性があります。155 個のアイテムをテストしたところ、そのほとんどに 30 個までの既存の予約があり、このクエリを実行するのに約 0.7 秒かかりました。
reservations
使用可能な項目ごとに完全なテーブルスキャンを実行するのを避けるために、インデックスを試してみましょう。直観的に、日付列にインデックスを付けることから始めるかもしれません。最終的にどのアイテムになるかは問題ではありませんが、適切な期間を確認することに非常に関心があります。
mysql> インデックスの作成 idx_startDate_endDate_item
-> ON 予約 (startDate,endDate,item);
クエリ OK、影響を受ける行は 0 (0.03 秒)
レコード: 0 重複: 0 警告: 0
残念ながら、これは期待したほど役に立ちません。MySQL は、 が値の狭い範囲内にしかstartDate BETWEEN @newReservationStart and @newReservationEnd
収まらないことを認識しているため、を非常にうまく処理します。startDate
しかし では@newReservationStart BETWEEN startDate and endDate
、狭い範囲に絞り込める単一の列を検索しているわけではありません。MySQL は、 より前に開始されたすべての予約を検索し@newReservationStart
、それらのどれが より後に終了するかを決定する必要があり@newReservationStart
ます。
同じ EXPLAIN ステートメントを実行すると、次のようになります。
+----+--------------------+--------------+-------+ ------------------+--------------------------------- -------+---------+------+------+------------------ --------------------------+
| | ID | select_type | テーブル | タイプ | 可能な_キー | キー | key_len | 参照 | 行 | 行 エクストラ |
+----+--------------------+--------------+-------+ ------------------+--------------------------------- -------+---------+------+------+------------------ --------------------------+
| | 1 | プライマリ | アイテム | すべて | ヌル | ヌル | ヌル | ヌル | 155 | where | の使用
| | 2 | 従属サブクエリ | 予約 | 範囲 | idx_startDate_endDate_item | idx_startDate_endDate_item | 4 | ヌル | 3572 | where を使用します。インデックスの使用; 一時的な使用 |
+----+--------------------+--------------+-------+ ------------------+--------------------------------- -------+---------+------+------+------------------ --------------------------+
インデックスにもかかわらず、3871 行から 3572 行までしか調べていませんitems.item
。ほとんどの予約が過去のものであると想定した場合、インデックス (endDate、startDate、item) を作成することで、もう少しうまくいく可能性があります。これは、endDate が @newReservationStart の後にある項目を調べることから始まり、小さなサブセットである可能性があります。しかし、それはまだ理想的ではありません。また、句startDate
の他の部分は特定の範囲の開始日を検索するため、最初の列として別のインデックスが必要になります。OR
ならどうしよう?
MySQL が の各値に対して内部クエリを実行することがわかっていますitems.item
。したがって、実際に必要なのは、現在調べているアイテムの予約を探すことだけです。これは、クエリを SQL 結合に変換することを意味する可能性がありますが、オプティマイザーにもう一度試してみましょう。
mysql> ALTER TABLE 予約 DROP INDEX idx_startDate_endDate_item;
クエリ OK、影響を受ける行は 0 (0.01 秒)
レコード: 0 重複: 0 警告: 0
mysql> CREATE INDEX idx_item_startDate
-> ON 予約 (item, startDate);
クエリ OK、影響を受ける行は 0 (0.02 秒)
レコード: 0 重複: 0 警告: 0
EXPLAIN ステートメントをもう一度実行すると、次のようになります。
+----+--------------------+--------------+-------- ------+--------------------+-------------------- +---------+------+------+------------------------- -----------+
| | ID | select_type | テーブル | タイプ | 可能な_キー | キー | key_len | 参照 | 行 | 行 エクストラ |
+----+--------------------+--------------+-------- ------+--------------------+-------------------- +---------+------+------+------------------------- -----------+
| | 1 | プライマリ | アイテム | すべて | ヌル | ヌル | ヌル | ヌル | 155 | where | の使用
| | 2 | 従属サブクエリ | 予約 | index_subquery | idx_item_startDate | idx_item_startDate | 5 | 関数 | 38 | where を使用します。NULL キーのフル スキャン |
+----+--------------------+--------------+-------- ------+--------------------+-------------------- +---------+------+------+------------------------- -----------+
悪くない、全く!楽しみのためにitems.item
、NOT NULL
. endDate
また、 がクエリで使用されているという事実を見落としていましたが、インデックスにはありません。MySQL は、ほとんどの作業でインデックスを使用します。endDate を確認するためだけにテーブル全体を参照する必要はないので、インデックスも置き換えましょう。
mysql> ALTER TABLE items MODIFY item INT NOT NULL;
クエリ OK、影響を受ける 155 行 (0.00 秒)
レコード: 155 重複: 0 警告: 0
mysql> ALTER TABLE 予約 DROP INDEX idx_item_startDate;
クエリ OK、影響を受ける行は 0 (0.00 秒)
レコード: 0 重複: 0 警告: 0
mysql> CREATE INDEX idx_item_startDate_endDate ON 予約 (項目、開始日、終了日);
クエリ OK、影響を受ける行は 0 (0.02 秒)
レコード: 0 重複: 0 警告: 0
そしてEXPLAIN
今、私たちに与えます:
+----+--------------------+--------------+-------- ------+--------------------------------+------------ -----+---------------------+------+------+--------- ------------------+
| | ID | select_type | テーブル | タイプ | 可能な_キー | キー | key_len | 参照 | 行 | 行 エクストラ |
+----+--------------------+--------------+-------- ------+--------------------------------+------------ -----+---------------------+------+------+--------- ------------------+
| | 1 | プライマリ | アイテム | すべて | ヌル | ヌル | ヌル | ヌル | 155 | where | の使用
| | 2 | 従属サブクエリ | 予約 | index_subquery | idx_item_startDate_endDate | idx_item_startDate_endDate | 5 | 関数 | 38 | インデックスの使用; where | の使用
+----+--------------------+--------------+-------- ------+--------------------------------+------------ -----+---------------------+------+------+--------- ------------------+
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
最後に、SQL での日付範囲の一致に関する質問からのStrawberryの回答を使用すると、ランタイムが最初のアプローチの約半分に削減されます。興味深いことに、出力はまったく同じです。しかし、以下に示す最後のクエリは、0.07 秒で実行されるようになりました。EXPLAIN
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