Sqlserver
 sql >> Base de Dados >  >> RDS >> Sqlserver

Encontrando eventos simultâneos em um banco de dados entre tempos


Isenção de responsabilidade:estou escrevendo minha resposta com base na (excelente) postagem a seguir:

https://www.itprotoday.com/sql-server/calculating-concurrent-sessions-part-3 (Parte 1 e 2 também são recomendadas)

A primeira coisa a entender aqui com esse problema é que a maioria das soluções atuais encontradas na internet podem ter basicamente dois problemas
  • O resultado não é a resposta correta (por exemplo, se o intervalo A se sobrepuser a B e C, mas B não se sobrepuser a C, eles contam como 3 intervalos sobrepostos).
  • A forma de calcular é muito ineficiente (porque é O(n^2) e/ou eles ciclam para cada segundo no período)

O problema de desempenho comum em soluções como a proposta por Unreasons é uma solução cuadrática, para cada chamada você precisa verificar todas as outras chamadas se estiverem sobrepostas.

existe uma solução comum linear algorítmica que é listar todos os "eventos" (chamada inicial e chamada final) ordenados por data e adicionar 1 para iniciar e subtrair 1 para desligar e lembre-se do max. Isso pode ser implementado facilmente com um cursor (a solução proposta por Hafhor parece ser dessa maneira), mas os cursores não são as maneiras mais eficientes de resolver problemas.

O artigo referenciado possui excelentes exemplos, diferentes soluções, comparação de desempenho das mesmas. A solução 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

Explicação

suponha que este conjunto de dados
+-------------------------+-------------------------+
|        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 |
+-------------------------+-------------------------+

Esta é uma forma de implementar com uma consulta a mesma ideia, adicionando 1 para cada início de uma chamada e subtraindo 1 para cada finalização.
  SELECT starttime AS ts, +1 AS TYPE,
    ROW_NUMBER() OVER(ORDER BY starttime) AS start_ordinal
  FROM Calls

esta parte do C1 CTE tomará cada hora de início de cada chamada e a numerará
+-------------------------+------+---------------+
|           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 |
+-------------------------+------+---------------+

Agora este código
  SELECT endtime, -1, NULL
  FROM Calls

Irá gerar todos os "endtimes" sem numeração de linha
+-------------------------+----+------+
|         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 |
+-------------------------+----+------+

Agora fazendo o UNION ter a definição completa do C1 CTE, você terá as duas tabelas misturadas
+-------------------------+------+---------------+
|           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 é calculado ordenando e numerando C1 com uma nova coluna
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 |
+-------------------------+------+-------+--------------+

E é aí que a mágica acontece, a qualquer momento o resultado de #start - #ends é a quantidade de chamadas simultâneas nesse momento.

para cada Type =1 (evento start) temos o valor #start na 3ª coluna. e também temos o #start + #end (na 4ª coluna)
#start_or_end = #start + #end

#end = (#start_or_end - #start)

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

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

então no SQL:
SELECT MAX(2 * start_ordinal - start_or_end_ordinal) AS mx
FROM C2
WHERE TYPE = 1

Neste caso com o conjunto proposto de chamadas, o resultado é 2.

No artigo proposto, há uma pequena melhoria para ter um resultado agrupado por exemplo por um serviço ou uma "companhia telefônica" ou "central telefônica" e essa ideia também pode ser usada para agrupar por exemplo por horário e ter o máximo de simultaneidade hora a hora em um determinado dia.