Sqlserver
 sql >> Database >  >> RDS >> Sqlserver

Trovare eventi simultanei in un database tra i tempi

Dichiarazione di non responsabilità:sto scrivendo la mia risposta in base al seguente post (eccellente):

https://www.itprotoday.com/sql-server/calculating-concurrent-sessions-part-3 (si consigliano anche le parti 1 e 2)

La prima cosa da capire qui con questo problema è che la maggior parte delle attuali soluzioni trovate su Internet possono avere fondamentalmente due problemi

  • Il risultato non è la risposta corretta (ad esempio se l'intervallo A si sovrappone a B e C ma B non si sovrappone a C, contano come 3 intervalli sovrapposti).
  • Il modo per calcolarlo è molto inefficiente (perché è O(n^2) e/o si alternano per ogni secondo nel periodo)

Il problema di prestazioni comune in soluzioni come quella proposta da Unreasons è una soluzione cuadratica, per ogni chiamata è necessario controllare tutte le altre chiamate se sono sovrapposte.

esiste una soluzione algoritmica lineare comune che è elencare tutti gli "eventi" (inizio chiamata e fine chiamata) ordinati per data, e aggiungere 1 per un inizio e sottrarre 1 per un riaggancio, e ricordare il max. Questo può essere implementato facilmente con un cursore (la soluzione proposta da Hafhor sembra essere in questo modo) ma i cursori non sono i modi più efficienti per risolvere i problemi.

L'articolo di riferimento ha esempi eccellenti, soluzioni diverse, confronto delle prestazioni di loro. La soluzione proposta è:

WITH C1 AS
(
  SELECT starttime AS ts, +1 AS TYPE,
    ROW_NUMBER() OVER(ORDER BY starttime) AS start_ordinal
  FROM Calls

  UNION ALL

  SELECT endtime, -1, NULL
  FROM Calls
),
C2 AS
(
  SELECT *,
    ROW_NUMBER() OVER(  ORDER BY ts, TYPE) AS start_or_end_ordinal
  FROM C1
)
SELECT MAX(2 * start_ordinal - start_or_end_ordinal) AS mx
FROM C2
WHERE TYPE = 1

Spiegazione

supponiamo questo insieme di dati

+-------------------------+-------------------------+
|        starttime        |         endtime         |
+-------------------------+-------------------------+
| 2009-01-01 00:02:10.000 | 2009-01-01 00:05:24.000 |
| 2009-01-01 00:02:19.000 | 2009-01-01 00:02:35.000 |
| 2009-01-01 00:02:57.000 | 2009-01-01 00:04:04.000 |
| 2009-01-01 00:04:12.000 | 2009-01-01 00:04:52.000 |
+-------------------------+-------------------------+

Questo è un modo per implementare con una query la stessa idea, aggiungendo 1 per ogni inizio di una chiamata e sottraendo 1 per ogni fine.

  SELECT starttime AS ts, +1 AS TYPE,
    ROW_NUMBER() OVER(ORDER BY starttime) AS start_ordinal
  FROM Calls

questa parte del C1 CTE prenderà ogni ora di inizio di ogni chiamata e la numererà

+-------------------------+------+---------------+
|           ts            | TYPE | start_ordinal |
+-------------------------+------+---------------+
| 2009-01-01 00:02:10.000 |    1 |             1 |
| 2009-01-01 00:02:19.000 |    1 |             2 |
| 2009-01-01 00:02:57.000 |    1 |             3 |
| 2009-01-01 00:04:12.000 |    1 |             4 |
+-------------------------+------+---------------+

Ora questo codice

  SELECT endtime, -1, NULL
  FROM Calls

Genererà tutti gli "endtime" senza numerazione delle righe

+-------------------------+----+------+
|         endtime         |    |      |
+-------------------------+----+------+
| 2009-01-01 00:02:35.000 | -1 | NULL |
| 2009-01-01 00:04:04.000 | -1 | NULL |
| 2009-01-01 00:04:52.000 | -1 | NULL |
| 2009-01-01 00:05:24.000 | -1 | NULL |
+-------------------------+----+------+

Ora facendo in modo che UNION abbia la definizione CTE completa C1, avrai entrambe le tabelle miste

+-------------------------+------+---------------+
|           ts            | TYPE | start_ordinal |
+-------------------------+------+---------------+
| 2009-01-01 00:02:10.000 |    1 |             1 |
| 2009-01-01 00:02:19.000 |    1 |             2 |
| 2009-01-01 00:02:57.000 |    1 |             3 |
| 2009-01-01 00:04:12.000 |    1 |             4 |
| 2009-01-01 00:02:35.000 | -1   |     NULL      |
| 2009-01-01 00:04:04.000 | -1   |     NULL      |
| 2009-01-01 00:04:52.000 | -1   |     NULL      |
| 2009-01-01 00:05:24.000 | -1   |     NULL      |
+-------------------------+------+---------------+

C2 è calcolato ordinando e numerando C1 con una nuova colonna

C2 AS
(
  SELECT *,
    ROW_NUMBER() OVER(  ORDER BY ts, TYPE) AS start_or_end_ordinal
  FROM C1
)

+-------------------------+------+-------+--------------+
|           ts            | TYPE | start | start_or_end |
+-------------------------+------+-------+--------------+
| 2009-01-01 00:02:10.000 |    1 | 1     |            1 |
| 2009-01-01 00:02:19.000 |    1 | 2     |            2 |
| 2009-01-01 00:02:35.000 |   -1 | NULL  |            3 |
| 2009-01-01 00:02:57.000 |    1 | 3     |            4 |
| 2009-01-01 00:04:04.000 |   -1 | NULL  |            5 |
| 2009-01-01 00:04:12.000 |    1 | 4     |            6 |
| 2009-01-01 00:04:52.000 |   -1 | NULL  |            7 |
| 2009-01-01 00:05:24.000 |   -1 | NULL  |            8 |
+-------------------------+------+-------+--------------+

Ed è lì che si verifica la magia, in qualsiasi momento il risultato di #inizio - #fine è la quantità di chiamate simultanee in questo momento.

per ogni Tipo =1 (evento di inizio) abbiamo il valore #start nella 3a colonna. e abbiamo anche #inizio + #fine (nella 4a colonna)

#start_or_end = #start + #end

#end = (#start_or_end - #start)

#start - #end = #start - (#start_or_end - #start)

#start - #end = 2 * #start - #start_or_end

quindi in SQL:

SELECT MAX(2 * start_ordinal - start_or_end_ordinal) AS mx
FROM C2
WHERE TYPE = 1

In questo caso con l'insieme di chiamate proposto, il risultato è 2.

Nell'articolo proposto, c'è un piccolo miglioramento per avere un risultato raggruppato ad esempio per un servizio o una "compagnia telefonica" o "centrale telefonica" e questa idea può essere utilizzata anche per raggruppare ad esempio per fascia oraria e avere la massima concorrenza ora per ora in un determinato giorno.