SQL - Algoritmo per la ricerca della disponibilità di una risorsa

6

Ho problemi a creare un algoritmo compatibile con mysql per questo.

Sfondo

App con mysql, perl e JS. È un sistema di prenotazione in cui ogni booking è composto da start , end e qty . L'inizio e la fine sono timestamp.

Schema semplificato in una singola tabella:

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

Domanda

In SQL, come controllate quante risorse sono state prenotate contemporaneamente per un dato timeRange ? Il codice con spiegazione o un algoritmo compatibile con SQL funzionano entrambi.

Quindi, per il seguente programma:

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

Dovrei ottenere 12 poiché le prenotazioni x2 e x10 non si sovrappongono alla prenotazione x7, quindi non ci sono mai più di 12 elementi prenotati contemporaneamente tra 9:00 e 11:30 .

Progress

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

Quale è questo algoritmo:

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

Nell'elenco sopra questo sarebbe:

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

Ma dato un programma come:

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

Questo dà:

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

Che è sbagliato.

L'unico modo in cui posso pensare di risolvere questo problema è usare la ricorsione (che non è un afaik compatibile con mysql).

Sembrerebbe qualcosa di simile (nel codice JS funzionante)

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

Demo Visual JSFiddle

Un modo per farlo in SQL? (qualsiasi modo ragionevole, cioè)

Aggiorna Ho appena provato a utilizzare un metodo di emulazione ricorsiva di stored procedure come documentato qui . Ma in parte l'ho implementato, l'ho provato con i dati demo e ho deciso che la performance era troppo terribile. In realtà, aveva solo bisogno dell'indicizzazione. Ora è solo un po 'brutto.

    
posta slicedtoad 25.02.2015 - 22:34
fonte

3 risposte

1

Quindi la soluzione di Esoteric ha funzionato, ma mi ha comunque infastidito dal momento che sembra un po 'bruteforce-ish. Sapevo che doveva esserci una soluzione che guardava solo i dati rilevanti ( start , end e qty ) e non aveva bisogno di tradurli in una forma diversa.

Poi ho ricordato order by e una soluzione mi ha colpito.

Tally bordo ordinato

  1. Crea un elenco di spigoli e le loro quantità (inizia con U alla fine). I bordi di fine ottengono i qty negati.
  2. Ordinali per data (per date duplicate, la fine va prima).
  3. Crea un totale parziale e combina le date duplicate.
+---------------------+-----------+-------+
| 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. Restituisci il conteggio massimo.

SQL reale:

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;

Precisione perfetta, assenza di join, complessità minima (le mie grandi capacità di notazione O mancano, forse O (2 * b) dove b è il numero di prenotazioni?)

% queryexplain:

+----+--------------+------------+-------+---------------+--------+---------+------+------+---------------------------------+
| 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                  |
+----+--------------+------------+-------+---------------+--------+---------+------+------+---------------------------------+
    
risposta data 27.02.2015 - 16:49
fonte
5

Questo è complicato, perché hai modellato le tue prenotazioni come intervalli di tempo con granularità come il DB ti consente. Perfettamente naturale da fare, ma come hai scoperto rende difficili i confronti.

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

Il problema con questo algoritmo è che controlla che ogni altro intervallo di prenotazione corrisponda a quello attualmente esaminato (l'iterazione foreach), ma non controlla le prenotazioni sovrapposte l'una contro l'altra, per vedere se si allineano. Un'esecuzione del secondo esempio è la seguente:

  • Seleziona 7x prenotazione
    • 7x non si sovrappone a 2x; 0
    • 7 volte si sovrappone a 10x; 10
    • Totale 17
  • Seleziona la prenotazione 2x
    • 2x non si sovrappone a 7x; 0
    • 2x sovrapposizioni con 10x; 10
    • Totale 12
  • Seleziona 10x prenotazione
    • 10 volte si sovrappone a 7x; 7
    • 10 volte si sovrappone a 2x; 2
    • [Passo mancante: controlla se 7x e 2x si sovrappongono]
    • Totale 19
  • Max 19

È ragionevolmente possibile mappare i tuoi dati su blocchi discreti, belli e ordinati di una certa dimensione? Ad esempio, le tue prenotazioni iniziano e finiscono generalmente il 15 (12:00, 12:15, 12:30, 12:45)? In tal caso, puoi modificare il tuo algoritmo per confrontare le prenotazioni con un intervallo di tempo statico, piuttosto che l'un l'altro, e ridurre drasticamente il numero richiesto di confronti:

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

In termini di implementazione SQL, scegli una dimensione di intervallo e utilizza una tabella di numeri o di conteggio per generare una query in linea per creare i blocchi:

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

(Off the cuff, potrebbe contenere errori di sintassi o di matematica minori)

Questo è un modo relativamente sano di eseguire questa query in SQL senza ricorsione. Ha l'ovvio inconveniente che non si allinea mai perfettamente con il tuo schema attuale, ma veramente ha bisogno della perfezione assoluta?

Ho usato 15 minuti come dimensioni di esempio. Puoi facilmente renderlo così fine come vuoi: 5 minuti, 1 minuto, 1 secondo, ecc. deve essere un punto in cui la granularità è troppo fine, perché il tipo di timestamp di MySQL non possiede precisione arbitraria. "Prenotare" per me implica qualcosa che coinvolge effettivamente gli uomini. Se questo è vero, non posso immaginare che una dimensione di intervallo inferiore a un minuto sia appropriata.

Nei commenti hai espresso preoccupazione per le prestazioni a causa di un numero elevato di confronti. La complessità di questo algoritmo è O (n * m), dove n è il numero di blocchi (intervallo di tempo / dimensione dell'intervallo) e m è il numero di righe di prenotazione nell'intervallo di tempo specificato. Mi azzarderò che in pratica, n > > m, il che significa che ciò che conta davvero per il tempo di calcolo è il numero di intervalli. Questo dovrebbe essere un non-problema, a patto che si utilizzino intervalli di tempo deboli e il DB sia indicizzato e mantenuto correttamente. Ad esempio, utilizzando un intervallo di un secondo per l'intervallo di tempo nella domanda (9:00 - 11:30) sono solo 9000 intervalli da ispezionare. 9000 righe è irrisorio per un server SQL. Confido che questo sia molto più performante di quanto non faccia usando l'SQL dinamico per emulare la ricorsione.

Se la dimensione dell'intervallo è 50 milioni di volte inferiore all'intervallo di tempo, allora sì, ci vorrà molto tempo per l'esecuzione (notare che non ho detto di comportarmi male), perché eseguirai una query contro 50 milioni di righe. Ma sta interrogando le prenotazioni massime per ogni millisecondo in un intervallo di dodici ore (43,2 milioni di ms) ragionevole e necessario? Ci sono solo 604800 secondi in una settimana. L'esecuzione di una query su un set di quelle dimensioni, anche se non banale, non dovrebbe dare ad alcun server SQL alcuna difficoltà.

Che aspetto hanno i tuoi dati? Quanto è bello un periodo di ispezione di cui hai bisogno? Se c'è un intervallo di due minuti (o secondo, decasecondo, millisecondo ...) in cui ci sono 105 prenotazioni anziché 100 perché qualcuno ha inserito un tempo di fine "insolito", distruggerà l'integrità del rapporto o potrà essere scartato come rumore? Non posso rispondere a queste domande, ma alcuni semplici dati e analisi dei requisiti da parte tua possono.

    
risposta data 26.02.2015 - 09:47
fonte
0

Questa è una possibilità con SQL, tuttavia è necessario generare una sequenza di numeri, dal momento che il server SQL che stavo testando questo non lo supporta ho dovuto recuperare la sequenza dalla funzione sys.all_objects ROW_NUMBER (),

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

l'approccio è quello di generare una vista con un numero di intervalli di tempo sufficientemente piccolo per i tempi di attesa del sistema di prenotazione più piccoli (in questo caso ho usato 5 minuti e puoi cambiarlo come vuoi)

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'

Quindi il valore della data passato alla funzione DATEADD sarebbe il tempo da e quello utilizzato nell'ultimo è l'ora di fine. Questo genererà risultati come questo,

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
.................

Un po 'fuori questione e puoi vedere la somma di ogni periodo da questa query

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

risulterà,

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..... 

Ora tutto ciò che devi fare è ottenere il valore massimo

    
risposta data 27.02.2015 - 02:17
fonte

Leggi altre domande sui tag