Las tareas de brechas e islas son desafíos de consulta clásicos en los que necesita identificar rangos de valores faltantes y rangos de valores existentes en una secuencia. La secuencia a menudo se basa en alguna fecha, o valores de fecha y hora, que normalmente deberían aparecer en intervalos regulares, pero faltan algunas entradas. La tarea de intervalos busca los períodos que faltan y la tarea de islas busca los períodos existentes. Cubrí muchas soluciones a lagunas y tareas de islas en mis libros y artículos en el pasado. Recientemente, mi amigo, Adam Machanic, me presentó un nuevo desafío de islas especiales, y resolverlo requirió un poco de creatividad. En este artículo presento el desafío y la solución que se me ocurrió.
El desafío
En su base de datos, realiza un seguimiento de los servicios que admite su empresa en una tabla llamada CompanyServices, y cada servicio normalmente informa aproximadamente una vez por minuto que está en línea en una tabla llamada EventLog. El siguiente código crea estas tablas y las completa con pequeños conjuntos de datos de muestra:
SET NOCOUNT ON; USE tempdb; IF OBJECT_ID(N'dbo.EventLog') IS NOT NULL DROP TABLE dbo.EventLog; IF OBJECT_ID(N'dbo.CompanyServices') IS NOT NULL DROP TABLE dbo.CompanyServices; CREATE TABLE dbo.CompanyServices ( serviceid INT NOT NULL, CONSTRAINT PK_CompanyServices PRIMARY KEY(serviceid) ); GO INSERT INTO dbo.CompanyServices(serviceid) VALUES(1), (2), (3); CREATE TABLE dbo.EventLog ( logid INT NOT NULL IDENTITY, serviceid INT NOT NULL, logtime DATETIME2(0) NOT NULL, CONSTRAINT PK_EventLog PRIMARY KEY(logid) ); GO INSERT INTO dbo.EventLog(serviceid, logtime) VALUES (1, '20180912 08:00:00'), (1, '20180912 08:01:01'), (1, '20180912 08:01:59'), (1, '20180912 08:03:00'), (1, '20180912 08:05:00'), (1, '20180912 08:06:02'), (2, '20180912 08:00:02'), (2, '20180912 08:01:03'), (2, '20180912 08:02:01'), (2, '20180912 08:03:00'), (2, '20180912 08:03:59'), (2, '20180912 08:05:01'), (2, '20180912 08:06:01'), (3, '20180912 08:00:01'), (3, '20180912 08:03:01'), (3, '20180912 08:04:02'), (3, '20180912 08:06:00'); SELECT * FROM dbo.EventLog;
La tabla EventLog se rellena actualmente con los siguientes datos:
logid serviceid logtime ----------- ----------- --------------------------- 1 1 2018-09-12 08:00:00 2 1 2018-09-12 08:01:01 3 1 2018-09-12 08:01:59 4 1 2018-09-12 08:03:00 5 1 2018-09-12 08:05:00 6 1 2018-09-12 08:06:02 7 2 2018-09-12 08:00:02 8 2 2018-09-12 08:01:03 9 2 2018-09-12 08:02:01 10 2 2018-09-12 08:03:00 11 2 2018-09-12 08:03:59 12 2 2018-09-12 08:05:01 13 2 2018-09-12 08:06:01 14 3 2018-09-12 08:00:01 15 3 2018-09-12 08:03:01 16 3 2018-09-12 08:04:02 17 3 2018-09-12 08:06:00
La tarea de las islas especiales es identificar los períodos de disponibilidad (servicio, hora de inicio, hora de finalización). Un problema es que no hay garantía de que un servicio informe que está en línea exactamente cada minuto; se supone que debe tolerar un intervalo de hasta, digamos, 66 segundos desde la entrada de registro anterior y aun así considerarlo parte del mismo período de disponibilidad (isla). Más allá de los 66 segundos, la nueva entrada de registro inicia un nuevo período de disponibilidad. Entonces, para los datos de muestra de entrada anteriores, se supone que su solución debe devolver el siguiente conjunto de resultados (no necesariamente en este orden):
serviceid starttime endtime ----------- --------------------------- --------------------------- 1 2018-09-12 08:00:00 2018-09-12 08:03:00 1 2018-09-12 08:05:00 2018-09-12 08:06:02 2 2018-09-12 08:00:02 2018-09-12 08:06:01 3 2018-09-12 08:00:01 2018-09-12 08:00:01 3 2018-09-12 08:03:01 2018-09-12 08:04:02 3 2018-09-12 08:06:00 2018-09-12 08:06:00
Observe, por ejemplo, cómo la entrada de registro 5 inicia una nueva isla ya que el intervalo desde la entrada de registro anterior es de 120 segundos (> 66), mientras que la entrada de registro 6 no inicia una nueva isla ya que el intervalo desde la entrada anterior es de 62 segundos ( <=66). Otro problema es que Adam quería que la solución fuera compatible con los entornos anteriores a SQL Server 2012, lo que hace que sea un desafío mucho más difícil, ya que no puede usar funciones de agregación de ventana con un marco para calcular totales acumulados y funciones de ventana de compensación. como LAG y LEAD. Como de costumbre, sugiero que intentes resolver el desafío tú mismo antes de ver mis soluciones. Utilice los pequeños conjuntos de datos de muestra para comprobar la validez de sus soluciones. Use el siguiente código para completar sus tablas con grandes conjuntos de datos de muestra (500 servicios, ~10 millones de entradas de registro para probar el rendimiento de sus soluciones):
-- Helper function dbo.GetNums IF OBJECT_ID(N'dbo.GetNums') IS NOT NULL DROP FUNCTION dbo.GetNums; GO CREATE FUNCTION dbo.GetNums(@low AS BIGINT, @high AS BIGINT) RETURNS TABLE AS RETURN WITH L0 AS (SELECT c FROM (SELECT 1 UNION ALL SELECT 1) AS D(c)), L1 AS (SELECT 1 AS c FROM L0 AS A CROSS JOIN L0 AS B), L2 AS (SELECT 1 AS c FROM L1 AS A CROSS JOIN L1 AS B), L3 AS (SELECT 1 AS c FROM L2 AS A CROSS JOIN L2 AS B), L4 AS (SELECT 1 AS c FROM L3 AS A CROSS JOIN L3 AS B), L5 AS (SELECT 1 AS c FROM L4 AS A CROSS JOIN L4 AS B), Nums AS (SELECT ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) AS rownum FROM L5) SELECT TOP(@high - @low + 1) @low + rownum - 1 AS n FROM Nums ORDER BY rownum; GO -- ~10,000,000 intervals DECLARE @numservices AS INT = 500, @logsperservice AS INT = 20000, @enddate AS DATETIME2(0) = '20180912', @validinterval AS INT = 60, -- seconds @normdifferential AS INT = 3, -- seconds @percentmissing AS FLOAT = 0.01; TRUNCATE TABLE dbo.EventLog; TRUNCATE TABLE dbo.CompanyServices; INSERT INTO dbo.CompanyServices(serviceid) SELECT A.n AS serviceid FROM dbo.GetNums(1, @numservices) AS A; WITH C AS ( SELECT S.n AS serviceid, DATEADD(second, -L.n * @validinterval + CHECKSUM(NEWID()) % (@normdifferential + 1), @enddate) AS logtime, RAND(CHECKSUM(NEWID())) AS rnd FROM dbo.GetNums(1, @numservices) AS S CROSS JOIN dbo.GetNums(1, @logsperservice) AS L ) INSERT INTO dbo.EventLog WITH (TABLOCK) (serviceid, logtime) SELECT serviceid, logtime FROM C WHERE rnd > @percentmissing;
Los resultados que proporcionaré para los pasos de mis soluciones asumirán los conjuntos pequeños de datos de muestra, y los números de rendimiento que proporcionaré asumirán los conjuntos grandes.
Todas las soluciones que presentaré se benefician del siguiente índice:
CREATE INDEX idx_sid_ltm_lid ON dbo.EventLog(serviceid, logtime, logid);
¡Buena suerte!
Solución 1 para SQL Server 2012+
Antes de cubrir una solución que es compatible con entornos anteriores a SQL Server 2012, cubriré una que requiere un mínimo de SQL Server 2012. La llamaré Solución 1.
El primer paso en la solución es calcular un indicador llamado isstart que es 0 si el evento no inicia una nueva isla y 1 en caso contrario. Esto se puede lograr utilizando la función LAG para obtener el tiempo de registro del evento anterior y verificando si la diferencia de tiempo en segundos entre el evento anterior y el actual es menor o igual que el intervalo permitido. Aquí está el código que implementa este paso:
DECLARE @allowedgap AS INT = 66; -- in seconds SELECT *, CASE WHEN DATEDIFF(second, LAG(logtime) OVER(PARTITION BY serviceid ORDER BY logtime, logid), logtime) <= @allowedgap THEN 0 ELSE 1 END AS isstart FROM dbo.EventLog;
Este código genera el siguiente resultado:
logid serviceid logtime isstart ----------- ----------- --------------------------- ----------- 1 1 2018-09-12 08:00:00 1 2 1 2018-09-12 08:01:01 0 3 1 2018-09-12 08:01:59 0 4 1 2018-09-12 08:03:00 0 5 1 2018-09-12 08:05:00 1 6 1 2018-09-12 08:06:02 0 7 2 2018-09-12 08:00:02 1 8 2 2018-09-12 08:01:03 0 9 2 2018-09-12 08:02:01 0 10 2 2018-09-12 08:03:00 0 11 2 2018-09-12 08:03:59 0 12 2 2018-09-12 08:05:01 0 13 2 2018-09-12 08:06:01 0 14 3 2018-09-12 08:00:01 1 15 3 2018-09-12 08:03:01 1 16 3 2018-09-12 08:04:02 0 17 3 2018-09-12 08:06:00 1
A continuación, un simple total acumulado de la bandera isstart produce un identificador de isla (lo llamaré grp). Aquí está el código que implementa este paso:
DECLARE @allowedgap AS INT = 66; WITH C1 AS ( SELECT *, CASE WHEN DATEDIFF(second, LAG(logtime) OVER(PARTITION BY serviceid ORDER BY logtime, logid), logtime) <= @allowedgap THEN 0 ELSE 1 END AS isstart FROM dbo.EventLog ) SELECT *, SUM(isstart) OVER(PARTITION BY serviceid ORDER BY logtime, logid ROWS UNBOUNDED PRECEDING) AS grp FROM C1;
Este código genera el siguiente resultado:
logid serviceid logtime isstart grp ----------- ----------- --------------------------- ----------- ----------- 1 1 2018-09-12 08:00:00 1 1 2 1 2018-09-12 08:01:01 0 1 3 1 2018-09-12 08:01:59 0 1 4 1 2018-09-12 08:03:00 0 1 5 1 2018-09-12 08:05:00 1 2 6 1 2018-09-12 08:06:02 0 2 7 2 2018-09-12 08:00:02 1 1 8 2 2018-09-12 08:01:03 0 1 9 2 2018-09-12 08:02:01 0 1 10 2 2018-09-12 08:03:00 0 1 11 2 2018-09-12 08:03:59 0 1 12 2 2018-09-12 08:05:01 0 1 13 2 2018-09-12 08:06:01 0 1 14 3 2018-09-12 08:00:01 1 1 15 3 2018-09-12 08:03:01 1 2 16 3 2018-09-12 08:04:02 0 2 17 3 2018-09-12 08:06:00 1 3
Por último, agrupe las filas por ID de servicio e identificador de isla y devuelva los tiempos de registro mínimo y máximo como la hora de inicio y la hora de finalización de cada isla. Aquí está la solución completa:
DECLARE @allowedgap AS INT = 66; WITH C1 AS ( SELECT *, CASE WHEN DATEDIFF(second, LAG(logtime) OVER(PARTITION BY serviceid ORDER BY logtime, logid), logtime) <= @allowedgap THEN 0 ELSE 1 END AS isstart FROM dbo.EventLog ), C2 AS ( SELECT *, SUM(isstart) OVER(PARTITION BY serviceid ORDER BY logtime, logid ROWS UNBOUNDED PRECEDING) AS grp FROM C1 ) SELECT serviceid, MIN(logtime) AS starttime, MAX(logtime) AS endtime FROM C2 GROUP BY serviceid, grp;
Esta solución tardó 41 segundos en completarse en mi sistema y produjo el plan que se muestra en la Figura 1.
Figura 1:Plan para la Solución 1
Como puede ver, ambas funciones de ventana se calculan según el orden del índice, sin necesidad de una clasificación explícita.
Si está utilizando SQL Server 2016 o posterior, puede usar el truco que cubro aquí para habilitar el operador de ventana agregada en modo por lotes mediante la creación de un índice de almacén de columnas filtrado vacío, así:
CREATE NONCLUSTERED COLUMNSTORE INDEX idx_cs ON dbo.EventLog(logid) WHERE logid = -1 AND logid = -2;
La misma solución ahora tarda solo 5 segundos en completarse en mi sistema, produciendo el plan que se muestra en la Figura 2.
Figura 2:Plan para la solución 1 usando el operador de ventana agregada en modo por lotes
Todo esto es genial, pero como se mencionó, Adam estaba buscando una solución que pudiera ejecutarse en entornos anteriores a 2012.
Antes de continuar, asegúrese de eliminar el índice del almacén de columnas para la limpieza:
DROP INDEX idx_cs ON dbo.EventLog;
Solución 2 para entornos anteriores a SQL Server 2012
Desafortunadamente, antes de SQL Server 2012, no teníamos soporte para funciones de ventana compensada como LAG, ni soporte para calcular totales acumulados con funciones de agregado de ventana con un marco. Esto significa que tendrás que trabajar mucho más duro para encontrar una solución razonable.
El truco que usé es convertir cada entrada de registro en un intervalo artificial cuya hora de inicio es la hora de registro de la entrada y cuya hora de finalización es la hora de registro de la entrada más el intervalo permitido. A continuación, puede tratar la tarea como una tarea de embalaje de intervalo clásico.
El primer paso de la solución calcula los delimitadores de intervalos artificiales y los números de fila que marcan las posiciones de cada uno de los tipos de eventos (counteach). Aquí está el código que implementa este paso:
DECLARE @allowedgap AS INT = 66; SELECT logid, serviceid, logtime AS s, -- important, 's' > 'e', for later ordering DATEADD(second, @allowedgap, logtime) AS e, ROW_NUMBER() OVER(PARTITION BY serviceid ORDER BY logtime, logid) AS counteach FROM dbo.EventLog;
Este código genera el siguiente resultado:
logid serviceid s e counteach ------ ---------- -------------------- -------------------- ---------- 1 1 2018-09-12 08:00:00 2018-09-12 08:01:06 1 2 1 2018-09-12 08:01:01 2018-09-12 08:02:07 2 3 1 2018-09-12 08:01:59 2018-09-12 08:03:05 3 4 1 2018-09-12 08:03:00 2018-09-12 08:04:06 4 5 1 2018-09-12 08:05:00 2018-09-12 08:06:06 5 6 1 2018-09-12 08:06:02 2018-09-12 08:07:08 6 7 2 2018-09-12 08:00:02 2018-09-12 08:01:08 1 8 2 2018-09-12 08:01:03 2018-09-12 08:02:09 2 9 2 2018-09-12 08:02:01 2018-09-12 08:03:07 3 10 2 2018-09-12 08:03:00 2018-09-12 08:04:06 4 11 2 2018-09-12 08:03:59 2018-09-12 08:05:05 5 12 2 2018-09-12 08:05:01 2018-09-12 08:06:07 6 13 2 2018-09-12 08:06:01 2018-09-12 08:07:07 7 14 3 2018-09-12 08:00:01 2018-09-12 08:01:07 1 15 3 2018-09-12 08:03:01 2018-09-12 08:04:07 2 16 3 2018-09-12 08:04:02 2018-09-12 08:05:08 3 17 3 2018-09-12 08:06:00 2018-09-12 08:07:06 4
El siguiente paso es descentrar los intervalos en una secuencia cronológica de eventos de inicio y finalización, identificados como tipos de eventos 's' y 'e', respectivamente. Tenga en cuenta que la elección de las letras s y e es importante ('s' > 'e'
). Este paso calcula los números de fila que marcan el orden cronológico correcto de ambos tipos de eventos, que ahora están intercalados (cuentan ambos). En caso de que un intervalo termine exactamente donde comienza otro, al colocar el evento de inicio antes del evento de finalización, los empaquetará juntos. Aquí está el código que implementa este paso:
DECLARE @allowedgap AS INT = 66; WITH C1 AS ( SELECT logid, serviceid, logtime AS s, -- important, 's' > 'e', for later ordering DATEADD(second, @allowedgap, logtime) AS e, ROW_NUMBER() OVER(PARTITION BY serviceid ORDER BY logtime, logid) AS counteach FROM dbo.EventLog ) SELECT logid, serviceid, logtime, eventtype, counteach, ROW_NUMBER() OVER(PARTITION BY serviceid ORDER BY logtime, eventtype DESC, logid) AS countboth FROM C1 UNPIVOT(logtime FOR eventtype IN (s, e)) AS U;
Este código genera el siguiente resultado:
logid serviceid logtime eventtype counteach countboth ------ ---------- -------------------- ---------- ---------- ---------- 1 1 2018-09-12 08:00:00 s 1 1 2 1 2018-09-12 08:01:01 s 2 2 1 1 2018-09-12 08:01:06 e 1 3 3 1 2018-09-12 08:01:59 s 3 4 2 1 2018-09-12 08:02:07 e 2 5 4 1 2018-09-12 08:03:00 s 4 6 3 1 2018-09-12 08:03:05 e 3 7 4 1 2018-09-12 08:04:06 e 4 8 5 1 2018-09-12 08:05:00 s 5 9 6 1 2018-09-12 08:06:02 s 6 10 5 1 2018-09-12 08:06:06 e 5 11 6 1 2018-09-12 08:07:08 e 6 12 7 2 2018-09-12 08:00:02 s 1 1 8 2 2018-09-12 08:01:03 s 2 2 7 2 2018-09-12 08:01:08 e 1 3 9 2 2018-09-12 08:02:01 s 3 4 8 2 2018-09-12 08:02:09 e 2 5 10 2 2018-09-12 08:03:00 s 4 6 9 2 2018-09-12 08:03:07 e 3 7 11 2 2018-09-12 08:03:59 s 5 8 10 2 2018-09-12 08:04:06 e 4 9 12 2 2018-09-12 08:05:01 s 6 10 11 2 2018-09-12 08:05:05 e 5 11 13 2 2018-09-12 08:06:01 s 7 12 12 2 2018-09-12 08:06:07 e 6 13 13 2 2018-09-12 08:07:07 e 7 14 14 3 2018-09-12 08:00:01 s 1 1 14 3 2018-09-12 08:01:07 e 1 2 15 3 2018-09-12 08:03:01 s 2 3 16 3 2018-09-12 08:04:02 s 3 4 15 3 2018-09-12 08:04:07 e 2 5 16 3 2018-09-12 08:05:08 e 3 6 17 3 2018-09-12 08:06:00 s 4 7 17 3 2018-09-12 08:07:06 e 4 8
Como se mencionó, counteach marca la posición del evento solo entre los eventos del mismo tipo, y countboth marca la posición del evento entre los eventos combinados e intercalados de ambos tipos.
Luego, la magia se maneja en el siguiente paso:calcular el conteo de intervalos activos después de cada evento en función de contar cada uno y contar ambos. El número de intervalos activos es el número de eventos de inicio que ocurrieron hasta el momento menos el número de eventos de finalización que ocurrieron hasta el momento. Para los eventos de inicio, contar cada uno le indica cuántos eventos de inicio ocurrieron hasta el momento, y puede calcular cuántos terminaron hasta el momento restando contar cada uno de contar ambos. Entonces, la expresión completa que le indica cuántos intervalos están activos es:
counteach - (countboth - counteach)
Para los eventos finales, counteach te dice cuántos eventos finales ocurrieron hasta el momento, y puedes calcular cuántos comenzaron restando counteach de countboth. Entonces, la expresión completa que le indica cuántos intervalos están activos es:
(countboth - counteach) - counteach
Con la siguiente expresión CASE, calcula la columna contable según el tipo de evento:
CASE WHEN eventtype = 's' THEN counteach - (countboth - counteach) WHEN eventtype = 'e' THEN (countboth - counteach) - counteach END
En el mismo paso, filtra solo los eventos que representan el inicio y el final de los intervalos empaquetados. Los inicios de los intervalos empaquetados tienen un tipo 's' y un 1 contable. Los finales de los intervalos empaquetados tienen un tipo 'e' y un 0 contable.
Después de filtrar, le quedan pares de eventos de inicio y finalización de intervalos empaquetados, pero cada par se divide en dos filas:una para el evento de inicio y otra para el evento de finalización. Por lo tanto, el mismo paso calcula el identificador de pares usando números de fila, con la fórmula (rownum – 1) / 2 + 1.
Aquí está el código que implementa este paso:
DECLARE @allowedgap AS INT = 66; WITH C1 AS ( SELECT logid, serviceid, logtime AS s, -- important, 's' > 'e', for later ordering DATEADD(second, @allowedgap, logtime) AS e, ROW_NUMBER() OVER(PARTITION BY serviceid ORDER BY logtime, logid) AS counteach FROM dbo.EventLog ), C2 AS ( SELECT logid, serviceid, logtime, eventtype, counteach, ROW_NUMBER() OVER(PARTITION BY serviceid ORDER BY logtime, eventtype DESC, logid) AS countboth FROM C1 UNPIVOT(logtime FOR eventtype IN (s, e)) AS U ) SELECT serviceid, eventtype, logtime, (ROW_NUMBER() OVER(PARTITION BY serviceid ORDER BY logtime, eventtype DESC, logid) - 1) / 2 + 1 AS grp FROM C2 CROSS APPLY ( VALUES( CASE WHEN eventtype = 's' THEN counteach - (countboth - counteach) WHEN eventtype = 'e' THEN (countboth - counteach) - counteach END ) ) AS A(countactive) WHERE (eventtype = 's' AND countactive = 1) OR (eventtype = 'e' AND countactive = 0);
Este código genera el siguiente resultado:
serviceid eventtype logtime grp ----------- ---------- -------------------- ---- 1 s 2018-09-12 08:00:00 1 1 e 2018-09-12 08:04:06 1 1 s 2018-09-12 08:05:00 2 1 e 2018-09-12 08:07:08 2 2 s 2018-09-12 08:00:02 1 2 e 2018-09-12 08:07:07 1 3 s 2018-09-12 08:00:01 1 3 e 2018-09-12 08:01:07 1 3 s 2018-09-12 08:03:01 2 3 e 2018-09-12 08:05:08 2 3 s 2018-09-12 08:06:00 3 3 e 2018-09-12 08:07:06 3
El último paso gira los pares de eventos en una fila por intervalo y resta el espacio permitido de la hora de finalización para generar la hora correcta del evento. Aquí está el código de la solución completa:
DECLARE @allowedgap AS INT = 66; WITH C1 AS ( SELECT logid, serviceid, logtime AS s, -- important, 's' > 'e', for later ordering DATEADD(second, @allowedgap, logtime) AS e, ROW_NUMBER() OVER(PARTITION BY serviceid ORDER BY logtime, logid) AS counteach FROM dbo.EventLog ), C2 AS ( SELECT logid, serviceid, logtime, eventtype, counteach, ROW_NUMBER() OVER(PARTITION BY serviceid ORDER BY logtime, eventtype DESC, logid) AS countboth FROM C1 UNPIVOT(logtime FOR eventtype IN (s, e)) AS U ), C3 AS ( SELECT serviceid, eventtype, logtime, (ROW_NUMBER() OVER(PARTITION BY serviceid ORDER BY logtime, eventtype DESC, logid) - 1) / 2 + 1 AS grp FROM C2 CROSS APPLY ( VALUES( CASE WHEN eventtype = 's' THEN counteach - (countboth - counteach) WHEN eventtype = 'e' THEN (countboth - counteach) - counteach END ) ) AS A(countactive) WHERE (eventtype = 's' AND countactive = 1) OR (eventtype = 'e' AND countactive = 0) ) SELECT serviceid, s AS starttime, DATEADD(second, -@allowedgap, e) AS endtime FROM C3 PIVOT( MAX(logtime) FOR eventtype IN (s, e) ) AS P;
Esta solución tardó 43 segundos en completarse en mi sistema y generó el plan que se muestra en la Figura 3.
Figura 3:Plan para la solución 2
Como puede ver, el cálculo del número de la primera fila se calcula según el orden del índice, pero los dos siguientes implican una clasificación explícita. Aun así, el rendimiento no es tan malo considerando que hay alrededor de 10 000 000 de filas involucradas.
Aunque el objetivo de esta solución es usar un entorno anterior a SQL Server 2012, solo por diversión, probé su rendimiento después de crear un índice de almacén de columnas filtrado para ver cómo funciona con el procesamiento por lotes habilitado:
CREATE NONCLUSTERED COLUMNSTORE INDEX idx_cs ON dbo.EventLog(logid) WHERE logid = -1 AND logid = -2;
Con el procesamiento por lotes habilitado, esta solución tardó 29 segundos en finalizar en mi sistema, produciendo el plan que se muestra en la Figura 4.
Conclusión
Es natural que cuanto más limitado sea su entorno, más desafiante se vuelve resolver tareas de consulta. El desafío especial de islas de Adam es mucho más fácil de resolver en las versiones más nuevas de SQL Server que en las más antiguas. Pero luego te obligas a usar técnicas más creativas. Entonces, como ejercicio, para mejorar sus habilidades de consulta, podría abordar desafíos con los que ya está familiarizado, pero imponer ciertas restricciones intencionalmente. ¡Nunca sabes con qué tipo de ideas interesantes te puedes topar!