sql >> Base de Datos >  >> RDS >> Sqlserver

Encontrar eventos simultáneos en una base de datos entre horas

Descargo de responsabilidad:estoy escribiendo mi respuesta en base a la (excelente) siguiente publicación:

https://www.itprotoday.com/sql-server/calculating-concurrent-sessions-part-3 (también se recomiendan las partes 1 y 2)

Lo primero que hay que entender aquí con ese problema es que la mayoría de las soluciones actuales que se encuentran en Internet pueden tener básicamente dos problemas

  • El resultado no es la respuesta correcta (por ejemplo, si el rango A se superpone con B y C, pero B no se superpone con C, cuentan como 3 rangos superpuestos).
  • La forma de calcularlo es muy ineficiente (porque es O(n^2) y/o ciclan por cada segundo en el período)

El problema de rendimiento común en soluciones como la propuesta por Unreasons es una solución cuadrática, para cada llamada debe verificar todas las demás llamadas si están superpuestas.

hay una solución algorítmica lineal común que es enumerar todos los "eventos" (llamada de inicio y llamada final) ordenados por fecha, y agregar 1 para un inicio y restar 1 para colgar, y recordar el máximo. Eso se puede implementar fácilmente con un cursor (la solución propuesta por Hafhor parece ser así), pero los cursores no son la forma más eficiente de resolver problemas.

El artículo al que se hace referencia tiene excelentes ejemplos, diferentes soluciones, comparación de rendimiento de los mismos. La solución propuesta es:

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

Explicación

supongamos que este conjunto de datos

+-------------------------+-------------------------+
|        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 es una forma de implementar con una consulta la misma idea, sumando 1 por cada inicio de una llamada y restando 1 por cada finalización.

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

esta parte del C1 CTE tomará cada hora de inicio de cada convocatoria y la 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 |
+-------------------------+------+---------------+

Ahora este código

  SELECT endtime, -1, NULL
  FROM Calls

Generará todos los "tiempos finales" sin numeración de filas

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

Ahora haciendo que UNION tenga la definición C1 CTE completa, tendrá ambas tablas mixtas

+-------------------------+------+---------------+
|           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 se calcula ordenando y numerando C1 con una nueva columna

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

Y ahí es donde ocurre la magia, en cualquier momento el resultado de #start - #ends es la cantidad de llamadas simultáneas en ese momento.

para cada Tipo =1 (evento de inicio) tenemos el valor de #inicio en la 3.ª columna. y también tenemos el #start + #end (en la 4ª columna)

#start_or_end = #start + #end

#end = (#start_or_end - #start)

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

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

entonces en SQL:

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

En este caso con el conjunto de llamadas propuesto, el resultado es 2.

En el artículo propuesto hay una pequeña mejora para tener un resultado agrupado por ejemplo por un servicio o una "compañía telefónica" o "central telefónica" y esta idea también se puede utilizar para agrupar por ejemplo por franja horaria y tener la máxima concurrencia hora por hora en un día determinado.