web-dev-qa-db-ja.com

SQL-リソースの可用性を見つけるためのアルゴリズム

このためのmysql互換アルゴリズムの作成に問題があります。

バックグラウンド

Mysql、Perl、JSを使用したアプリ。これは予約システムであり、各bookingstartendおよびqtyで構成されています。開始と終了はタイムスタンプです。

単一のテーブルに簡略化されたスキーマ:

|  bookings        
|-------------------
| id    | pkey      
| start | timestamp 
| end   | timestamp 
| qty   | int       

質問

SQLでは、特定のtimeRangeに対して一度に予約されているリソースの数をどのように確認しますか?説明付きのコードまたはSQL互換のアルゴリズムはどちらも機能します。

したがって、次のスケジュールの場合:

09:00 -----               <-|
09:30 |   |                 | A maximum of 12 are booked at once during this range
10:00 |x7 |                 | 
10:30 ----- ----- -----     |
11:00       |   | |   |     |                       
11:30       |x2 | |x10|   <-|
12:00       |   | |   |
12:30       ----- -----

X2とx10の予約はx7の予約と重複しないため、12を取得する必要があります。したがって、9:00および11:30

進捗

--It's been heavily shrunk to show just the relevant part, so it might have some errors
SELECT coalesce(max(qtyOverlap.sum),0) booked
FROM (
    SELECT coalesce(sum(b2.qty),0) sum
        FROM booking b1
        LEFT JOIN (
            SELECT b.qty, b.tStart, b.tEnd FROM booking b
        ) b2
        ON b1.tStart < b2.tEnd AND
           b1.tEnd > b2.tStart AND
           b2.tStart < '2015-02-19 16:30:00' AND
           b2.tEnd > '2015-02-19 06:00:00'
        WHERE 
              b1.tStart < '2015-02-19 16:30:00' AND
              b1.tEnd > '2015-02-19 06:00:00'
        GROUP BY b1.id
) qtyOverlap
GROUP BY qtyOverlap.itemId

これはこのアルゴリズムです:

Max of
    For each booking that overlaps given timeRange
        return sum of
            each booking that overlaps this booking and given timeRange

上記のスケジュールでは、これは次のようになります。

max([7],[2+10],[10+2]) = 12

しかし、次のようなスケジュールが与えられたとします。

09:00 -----               <-|
09:30 |   |                 | A maximum of 17 are booked at once during this range, not 19
10:00 |x7 |                 | 
10:30 |   |       -----     |
11:00 -----       |   |     |                       
11:30       ----- |x10|   <-|
12:00       |x2 | |   |
12:30       ----- -----

これは与える:

max([7+10],[2+10],[10+7+2]) = 19

それは間違っています。

これを修正するために私が考えることができる唯一の方法は、再帰を使用することです(これはmysql互換のafaikではありません)。

それは(実際のJSコードでは)次のようになります

function getOverlaps(bookings,range) {
    return bookings.filter(function(booking){
        return isOverLapping(booking,range);
    });
}
function isOverLapping(a, b) {
    return (a.start < b.end && a.end > b.start);
}
function maxSum(booking, overlaps) { // main recursive function
    var currentMax = 0;
    var filteredOverlaps = getOverlaps(overlaps,booking);
    for (var i = 0; i < filteredOverlaps.length; i++) {
        currentMax = Math.max(
            maxSum(filteredOverlaps[i], removeElement(filteredOverlaps,i)),
            currentMax
        );
    }
    return currentMax + booking.qty;
}
function removeElement(array,i){
    var clone = array.slice(0)
    clone.splice(i,1);
    return clone;
}
var maxBooked = maxSum(timeRange, getOverlaps(bookings,timeRange));

Visual JSFiddleデモ

これをSQLで行う方法はありますか? (合理的な方法、つまり)

Updateドキュメントに記載されているように、ストアドプロシージャの再帰エミュレーションメソッドを使用しようとしました here 。しかし、それを実装する途中で、私はそれをデモデータで試して、 パフォーマンスはひどすぎた。 実際には、索引付けだけが必要でした。今、それはただ「ちょっと」悪いだけです。

6
DanielST

Esotericの解決策はうまくいきましたが、それはちょっとブルートフォースっぽい感じがするので、それでも私を悩ませました。関連するデータ(startendおよびqty)のみを参照するソリューションが必要であり、それを別の形式に変換する必要がないことを知っていました。

それから私はorder byと解決策が私を襲った。

順序付きエッジ集計

  1. エッジとそれらの数量のリストを作成します(U方向の開始)。終了エッジは数量を否定します。
  2. 日付順に並べます(日付が重複している場合は、最初に終了します)。
  3. 現在の合計を作成し、重複する日付を結合します。
+---------------------+-----------+-------+
| edgedate            | qtyChange | tally |
+---------------------+-----------+-------+
| 2015-02-19 09:00:00 |         7 |     7 |
| 2015-02-19 10:30:00 |        10 |    17 |
| 2015-02-19 11:00:00 |        -7 |    10 |
| 2015-02-19 11:30:00 |         2 |    12 |
| 2015-02-19 12:30:00 |       -12 |    10 |
+---------------------+-----------+-------+

4.最大タリーを返します。

実際のSQL:

SET @i = 0;
SELECT max(Edge.tally)
    FROM (
        SELECT sum(@i:= b1.qty + @i) AS tally /*Cumulative sum and combine any duplicate dates*/
            FROM ( /*Get every Edge (start U end)*/
                SELECT tstart, qty, 1 as ord
                    FROM booking b
                    WHERE b.tstart < '2015-02-19 12:30:00' AND
                          b.tend   > '2015-02-19 08:00:00'
                UNION
                SELECT tend AS tstart, (qty*-1) AS qty, 0 as ord /*End edges have negative qtys*/
                    FROM booking b
                    WHERE b.tstart < '2015-02-19 12:30:00' AND
                          b.tend   > '2015-02-19 08:00:00'
                ORDER BY tstart, ord
            ) b1
            GROUP BY b1.tstart
    ) Edge;

完璧な精度、結合なし、最小限の複雑さ(私の大きなO表記法のスキルが不足している、おそらくO(2 * b)ここで、bは予約の数ですか?)

explainクエリ:

+----+--------------+------------+-------+---------------+--------+---------+------+------+---------------------------------+
| id | select_type  | table      | type  | possible_keys | key    | key_len | ref  | rows | Extra                           |
+----+--------------+------------+-------+---------------+--------+---------+------+------+---------------------------------+
|  1 | PRIMARY      | <derived2> | ALL   | NULL          | NULL   | NULL    | NULL |    5 |                                 |
|  2 | DERIVED      | <derived3> | ALL   | NULL          | NULL   | NULL    | NULL |    6 | Using temporary; Using filesort |
|  3 | DERIVED      | b          | range | tstart,tend   | tstart | 9       | NULL |    2 | Using where                     |
|  4 | UNION        | b          | range | tstart,tend   | tstart | 9       | NULL |    2 | Using where                     |
| NULL | UNION RESULT | <union3,4> | ALL   | NULL          | NULL   | NULL    | NULL | NULL | Using filesort                  |
+----+--------------+------------+-------+---------------+--------+---------+------+------+---------------------------------+
1
DanielST

DBが許す限り細かい時間間隔で予約をモデル化しているため、これはトリッキーです。実行するのは完全に自然ですが、ご存知のように、一部の比較が困難になります。

Max of
    For each booking that overlaps given timeRange
        return sum of
            each booking that overlaps this booking and given timeRange

このアルゴリズムの問​​題は、他の各予約間隔が現在調査されている間隔(foreach反復)と一致することを確認しますが、重複する予約を相互に確認して、一致するかどうかを確認しないことです。 2番目の例の実行は次のようになります。

  • 7倍の予約を選択してください
    • 7xは2xと重複しません。 +0
    • 7xは10xと重複します。 +10
    • 合計17
  • 2倍の予約を選択してください
    • 2xは7xと重複しません。 +0
    • 2xは10xと重複します。 +10
    • 合計12
  • 10倍の予約を選択してください
    • 10xは7xと重複します。 +7
    • 10xは2xと重複します。 +2
    • [不足している手順:7xと2xが重複していないか確認してください]
    • 合計19
  • 最大19

データをニースで適切なサイズの個別のチャンクにマップすることは合理的に可能ですか?たとえば、予約は通常15時に始まり、終わりますか(12:00、12:15、12:30、12:45)?その場合は、アルゴリズムを変更して、予約を互いに比較するのではなく、静的な時間間隔に対して比較し、必要な比較の数を大幅に減らすことができます。

Max of
  For each 15 minute chunk in timeRange
    Sum quantities of all bookings overlapping this chunk

SQL実装に関しては、間隔サイズを選択し、数値または集計テーブルを使用して、インラインクエリを生成してチャンクを作成します。

select @startTime + interval (15 * numbers.value) minute as start
, @startTime + interval (15 * (numbers.value + 1)) minute as end
from numbers
where (@startTime + interval (15 * numbers.value) minute) < @endTime

(袖口外、軽度の構文エラーまたは数学エラーが含まれる場合があります)

これは、再帰せずにSQLでこのクエリを実行する比較的健全な方法です。現在のスキーマと完全に一致することは決してないという明らかな欠点がありますが、本当に完全な完全性が必要ですか?

サイズの例として15分を使用しました。これは、5分、1分、1秒など、好きなように簡単に細かくすることができます。MySQLのタイムスタンプタイプが所有していないため、粒度が細かすぎる点がありますmust任意精度。私にとって「予約」とは、人間が実際に現れることを意味します。これに該当する場合、1分未満の間隔サイズが適切であることは想像できません。

コメントでは、多数の比較が行われるため、パフォーマンスに関する懸念を表明しました。このアルゴリズムの複雑さはO(n * m)です。ここで、nはチャンクの数(時間範囲/間隔サイズ)で、mは指定された時間範囲内の予約行の数です。実際には、n >> mという危険があります。つまり、計算時間に本当に重要なのは、間隔の数です。正常なタイムフレームを使用し、DBにインデックスが付けられ、正しく維持されている限り、これは問題にはなりません。たとえば、質問の時間範囲(9:00-11:30)に1秒の間隔サイズを使用すると、検査する間隔は9000のみになります。 9000行はSQLサーバーにとってはわずかです。これは、動的SQLを使用して再帰をエミュレートするよりもはるかに優れていると信頼しています。

間隔のサイズが時間範囲の5,000万分の1である場合、はい、実行にはかなりの時間がかかります(パフォーマンスが悪いとは言わなかったことに注意してください)。これは、5,000万行に対してクエリを実行するためです。 。しかし、最大予約数のクエリは12時間のスパンでミリ秒ごと(4320万ms)であり、妥当かつ必要ですか。週には604800秒しかありません。そのサイズのセットでクエリを実行することは、ささいなことではありませんが、SQLサーバーに問題を与えることはありません。

データはどのように見えますか?検査期間はどのくらい細かいですか?誰かが「異常な」終了時間を入力したために100ではなく105の予約がある2分(または秒、デカ秒、ミリ秒...)の間隔がある場合、レポートの整合性が失われるか、そのデータが破棄される可能性があります。ノイズとして?私はこれらの質問に答えることはできませんが、あなたの側のいくつかの簡単なデータと要件分析はできます。

これはSQLの可能性ですが、一連の数値を生成する必要があります。SQLサーバーでこれをテストしていたため、sys.all_objects ROW_NUMBER()関数からシーケンスをフェッチする必要があったため、これをサポートしていません。

SELECT n = ROW_NUMBER() OVER (ORDER BY [object_id]) 
FROM sys.all_objects 

アプローチは、予約システムの最小許容時間スロットに十分な時間間隔の数を持つビューを生成することです(この場合、5分を使用し、必要に応じて変更できます)。

select 
DATEADD(MINUTE, 5 * n, '2015-02-19 08:00:00') t_start,
DATEADD(MINUTE, 5 * (n + 1), '2015-02-19 08:00:00') t_end 
from 
bookings b,
(
  SELECT n = ROW_NUMBER() OVER (ORDER BY [object_id]) 
  FROM sys.all_objects 
) numbers
where 
DATEADD(MINUTE, 5 * (n + 1), '2015-02-19 08:00:00') < '2015-02-19 17:00:00'

したがって、DATEADD関数に渡される日付値は開始時刻であり、最後に使用されるのは終了時刻です。これはこのような結果セットを生成します、

t_start                 |  t_end
--------------------------------------------------
2015-02-19 08:05:00.000 |  2015-02-19 08:10:00.000
2015-02-19 08:10:00.000 |  2015-02-19 08:15:00.000
2015-02-19 08:15:00.000 |  2015-02-19 08:20:00.000
.................

少し疑問に思うと、このクエリから各期間の合計を確認できます

select 
tInt.t_start,
tInt.t_end,
(select sum(b.qty) from bookings b where b.tstart <= tInt.t_start and b.tend >= tInt.t_end) as total
from
(
select 
DATEADD(MINUTE, 5 * n, '2015-02-19 08:00:00') t_start,
DATEADD(MINUTE, 5 * (n + 1), '2015-02-19 08:00:00') t_end 
from 
bookings b,
(
  SELECT n = ROW_NUMBER() OVER (ORDER BY [object_id]) 
  FROM sys.all_objects 
) numbers
where 
DATEADD(MINUTE, 5 * (n + 1), '2015-02-19 08:00:00') < '2015-02-19 17:00:00'
)
tInt

結果は、

t_start                | t_end                      |total
-----------------------------------------------------------
2015-02-19 08:55:00.000|    2015-02-19 09:00:00.000 |NULL
same repeating.....    |                            |
2015-02-19 09:00:00.000|    2015-02-19 09:05:00.000 |7
same repeating.....    |                            |
2015-02-19 10:30:00.000|    2015-02-19 10:35:00.000 |NULL
same repeating.....    |                            |
2015-02-19 11:00:00.000|    2015-02-19 11:05:00.000 |10
same repeating.....    |                            |
2015-02-19 12:00:00.000|    2015-02-19 12:05:00.000 |12
same repeating.....    |                            |
2015-02-19 12:30:00.000|    2015-02-19 12:35:00.000 |NULL
same repeating..... 

今あなたがしなければならないすべては最大値を取得することです

0