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.