3

これはよくある質問のようですが、ウェブで検索しても答えが見つかりません。

何日か予約したいので(部分的な日はありません)、次のようなテーブルが必要だと思います:

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

(うーん、私の主キーは何ですか? item と startDate? PK も必要ですか?)

しかし、私の主な質問は、開始日と終了日を指定して無料アイテムを見つける方法です。私はどのSELECT ...ように見えますか?

ボーナス マークについては、すべてのアイテムが同一であり、これをできるだけ効率的にしたいと仮定して、金曜日から予約したい場合は、木曜日まで予約されているアイテムを見つけたいと思います (したがって、金曜日から無料)。

ボーナス マークが 2 倍の場合、アイテムが X 日間必要な場合、予約に空きがあるアイテムをできるだけ X 日間近く探したいと考えています。

問題は、そこにないもの (既存の予約) を見つけようとしていることだと思います。私が見つけた他のすべてのソリューションには、アイテムID(「まだ予約されていない」ことを意味するNULL、0または-1の値)を持つ予約可能な日付のテーブルがあるようです。それは私には非効率に思えます。そして、このテーブルは将来どのくらいまで拡張されるでしょうか?

注: 一部の人々は、読み取りと書き込みの比率について質問しています。明らかに、各予約は 1 回だけ行われるため、書き込みは 1 回 (実装によっては 1 日 1 回) であり、ユーザーが予約されていないスロットを検索すると、複数回の読み取りが予想されます。

4

6 に答える 6

6
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

テストするスキーマはありませんが、このクエリは最初のボーナスの答えになるはずです

2番目に関しては、これには1つのテーブルの行間でいくつかの計算を行う必要があり、可能であれば純粋なMySQLでそれを行う方法が思い浮かびません。

//編集

既存の予約が検索期間の前に開始し、検索期間後に終了する場合のシナリオの条件をもう 1 つ追加して、クエリを更新しました。

2番目のボーナス質問では、これが機能するはずです

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

しかし、これは非常に高価なクエリになります。サブクエリの日付から 1 日を加算/減算する必要がある場合があります。

ご覧のとおり、投稿の最初からクエリをサブクエリとして使用しましたが、1 回目と 2 回目のクエリは一度だけ実行されるため、大きな問題にはなりません。2 番目のボーナスの最後のクエリでは、行ごとに個別に実行する必要があり (結合があるため、アイテムごとに、特定のアイテムの予約数の 2 乗)、ボトルネックになる可能性があります。

あなたが予約しようとしているそれらのアイテムが何であるかはわかりませんが、それらが1000未満の場合は十分に高速かもしれません(年間最大365000行になります)が、アイテムの数が本当に大きくなる場合はおそらくあなた将来の最大 1 年に見えるように追加の条件を作成し、必要な場合にのみこれを増やすことができます。また、パーティショニングを行うと、かなり高速に動作する可能性があります。

于 2013-04-14T05:29:25.047 に答える
4

私のアプローチにとって重要ではありませんが、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.itemNOT 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
于 2013-04-18T21:08:28.220 に答える
1

ルックアップの効率が最重要である場合は、次のようなスキーマを使用する方がよいでしょう...

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)
);

...そして、アイテムが予約されている日付ごとに個別の行を追加します。

このように、DB は、同じ日に同じアイテムを 2 回以上予約できないようにし、どのアイテム 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 |
+----+-------------+-------+--------+---------------+---------+---------+-----------------+------+--------------------------------------+

これは、予約を追加/変更するときにもう少し作業が必要であることを意味しますが、書き込みよりも多くの読み取りを行うと仮定すると、おそらく大きなオーバーヘッドにはなりません。

于 2013-04-18T12:40:08.067 に答える
1

他の人が指摘したように、これはあまり効率的である必要はないかもしれません。そうは言っても、ここに1つのアプローチがあります(読み取りと書き込みの比率に応じて):

1 つの方法は、予約されていない時間のブロックを追跡するテーブルを作成することです (関心のある期間、たとえば 2000 年から 2020 年まで)。最初は、アイテムごとに1ブロックの自由時間になります。(これを読みやすい方法でリストします。スキーマはご想像にお任せします。)

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

RESERVATIONS
(none)

誰かが予約したら、予約を作成し、空きスロットを 2 つの小さな空きスロットに分割します (空になる場合を除く)。この操作中は、データ ストアのロックに注意してください。

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

予約が取り除かれると、直前と直後の空きスロットを確認します。両方が存在する場合は、両方の空きスロットと予約を 1 つの空きスロットに結合します。1 つしか存在しない場合は、それを拡張して、予約によって以前に占有されていたスペースを埋めます。

空いているスロットの期間をテーブルに簡単に保持できるため、目的の期間のスロットを簡単に見つけることができます (厳密に、一定量以上、範囲内など)。支払うコストは、データ ストアを変更するときに一貫性を確保するために必要なロックです。

于 2013-04-14T04:37:48.890 に答える
1

これは、データベースの外で行うのは非常に簡単です。予約を検討する期間のすべての予約を選択し、その結果を使用して、1 日が 1 (満室) または 0 (満室) のいずれかである配列を設定します。 ) 配列をスキャンして、目的のサイズのギャップを探します。O(n) ですが、1 年は 365 日しかないので遅くはありません。

于 2013-04-11T01:49:27.650 に答える