Subconsulta de bajo rendimiento con comparaciones de fechas


15

Cuando se utiliza una subconsulta para encontrar el recuento total de todos los registros anteriores con un campo coincidente, el rendimiento es terrible en una tabla con tan solo 50k registros. Sin la subconsulta, la consulta se ejecuta en unos pocos milisegundos. Con la subconsulta, el tiempo de ejecución es más de un minuto.

Para esta consulta, el resultado debe:

  • Incluya solo aquellos registros dentro de un rango de fechas dado.
  • Incluya un recuento de todos los registros anteriores, sin incluir el registro actual, independientemente del rango de fechas.

Esquema de tabla básico

Activity
======================
Id int Identifier
Address varchar(25)
ActionDate datetime2
Process varchar(50)
-- 7 other columns

Datos de ejemplo

Id  Address     ActionDate (Time part excluded for simplicity)
===========================
99  000         2017-05-30
98  111         2017-05-30
97  000         2017-05-29
96  000         2017-05-28
95  111         2017-05-19
94  222         2017-05-30

Resultados previstos

Para el rango de fechas de 2017-05-29a2017-05-30

Id  Address     ActionDate    PriorCount
=========================================
99  000         2017-05-30    2  (3 total, 2 prior to ActionDate)
98  111         2017-05-30    1  (2 total, 1 prior to ActionDate)
94  222         2017-05-30    0  (1 total, 0 prior to ActionDate)
97  000         2017-05-29    1  (3 total, 1 prior to ActionDate)

Los registros 96 y 95 se excluyen del resultado, pero se incluyen en la PriorCountsubconsulta

Consulta actual

select 
    *.a
    , ( select count(*) 
        from Activity
        where 
            Activity.Address = a.Address
            and Activity.ActionDate < a.ActionDate
    ) as PriorCount
from Activity a
where a.ActionDate between '2017-05-29' and '2017-05-30'
order by a.ActionDate desc

Índice actual

CREATE NONCLUSTERED INDEX [IDX_my_nme] ON [dbo].[Activity]
(
    [ActionDate] ASC
)
INCLUDE ([Address]) WITH (
    PAD_INDEX = OFF, 
    STATISTICS_NORECOMPUTE = OFF, 
    SORT_IN_TEMPDB = OFF, 
    DROP_EXISTING = OFF, 
    ONLINE = OFF, 
    ALLOW_ROW_LOCKS = ON, 
    ALLOW_PAGE_LOCKS = ON
)

Pregunta

  • ¿Qué estrategias podrían usarse para mejorar el rendimiento de esta consulta?

Edición 1
En respuesta a la pregunta de qué puedo modificar en la base de datos: puedo modificar los índices, pero no la estructura de la tabla.

Edición 2
Ahora he agregado un índice básico en la Addresscolumna, pero eso no parece mejorar mucho. Actualmente estoy encontrando un rendimiento mucho mejor con la creación de una tabla temporal e insertando los valores sin el PriorCounty luego actualizando cada fila con sus recuentos específicos.

Editar 3
El índice Spool Joe Obbish (respuesta aceptada) encontrado fue el problema. Una vez que agregué una nueva nonclustered index [xyz] on [Activity] (Address) include (ActionDate), los tiempos de consulta disminuyeron de más de un minuto a menos de un segundo sin usar una tabla temporal (ver edición 2).

Respuestas:


17

Con la definición de índice que tiene IDX_my_nme, SQL Server podrá buscar utilizando la ActionDatecolumna pero no con la Addresscolumna. El índice contiene todas las columnas necesarias para cubrir la subconsulta, pero probablemente no sea muy selectivo para esa subconsulta. Suponga que casi todos los datos de la tabla tienen un ActionDatevalor anterior a '2017-05-30'. Una búsqueda de ActionDate < '2017-05-30'devolverá casi todas las filas del índice, que se filtran aún más después de que la fila se recupera del índice. Si su consulta devuelve 200 filas, entonces probablemente haría casi 200 escaneos de índice completo IDX_my_nme, lo que significa que leerá alrededor de 50000 * 200 = 10 millones de filas del índice.

Es probable que la búsqueda Addresssea ​​mucho más selectiva para su subconsulta, aunque no nos ha proporcionado información estadística completa sobre la consulta, por lo que es una suposición de mi parte. Sin embargo, suponga que crea un índice en just Addressy su tabla tiene 10k valores únicos para Address. Con el nuevo índice, SQL Server solo necesitará buscar 5 filas del índice para cada ejecución de la subconsulta, por lo que leerá alrededor de 200 * 5 = 1000 filas del índice.

Estoy probando contra SQL Server 2016, por lo que puede haber algunas pequeñas diferencias de sintaxis. A continuación se muestran algunos datos de muestra en los que hice suposiciones similares a las anteriores para la distribución de datos:

CREATE TABLE #Activity (
    Id int NOT NULL,
    [Address] varchar(25) NULL,
    ActionDate datetime2 NULL,
    FILLER varchar(100),
    PRIMARY KEY (Id)
);

INSERT INTO #Activity WITH (TABLOCK)
SELECT TOP (50000) -- 50k total rows
x.RN
, x.RN % 10000 -- 10k unique addresses
, DATEADD(DAY, x.RN / 100, '20160201') -- 100 rows per day
, REPLICATE('Z', 100)
FROM
(
    SELECT ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) RN
    FROM master..spt_values t1
    CROSS JOIN master..spt_values t2
) x;

CREATE NONCLUSTERED INDEX [IDX_my_nme] ON #Activity
([ActionDate] ASC) INCLUDE ([Address]);

He creado su índice como se describe en la pregunta. Estoy probando contra esta consulta que devuelve los mismos datos que en la pregunta:

select 
    a.*
    , ( select count(*) 
        from #Activity Activity
        where 
            Activity.[Address] = a.[Address]
            and Activity.ActionDate < a.ActionDate
    ) as PriorCount
from #Activity a
where a.ActionDate between '2017-05-29' and '2017-05-30'
order by a.ActionDate desc;

Me sale un carrete de índice. Lo que eso significa en un nivel básico es que el optimizador de consultas crea un índice temporal sobre la marcha porque ninguno de los índices existentes en la tabla era adecuado.

carrete de índice

La consulta aún termina rápidamente para mí. Quizás no esté obteniendo la optimización del spool de índice en su sistema o haya algo diferente sobre la definición de la tabla o la consulta. Para fines educativos, puedo usar una función no documentada OPTION (QUERYRULEOFF BuildSpool)para deshabilitar el carrete de índice. Así es como se ve el plan:

mala búsqueda de índice

No se deje engañar por la apariencia de una simple búsqueda de índice. SQL Server lee casi 10 millones de filas del índice:

10 millones de filas desde el índice

Si voy a ejecutar la consulta más de una vez, entonces probablemente no tenga sentido que el optimizador de consultas cree un índice cada vez que se ejecuta. Podría crear un índice por adelantado que sería más selectivo para esta consulta:

CREATE NONCLUSTERED INDEX [IDX_my_nme_2] ON #Activity
([Address] ASC) INCLUDE (ActionDate);

El plan es similar al anterior:

búsqueda de índice

Sin embargo, con el nuevo índice, SQL Server solo lee 1000 filas del índice. 800 de las filas se devuelven para ser contadas. El índice podría definirse como más selectivo, pero podría ser lo suficientemente bueno según su distribución de datos.

buena búsqueda

Si no puede definir ningún índice adicional en la tabla, consideraría usar funciones de ventana. Lo siguiente parece funcionar:

SELECT t.*
FROM
(
    select 
        a.*
        , -1 + ROW_NUMBER() OVER (PARTITION BY [Address] ORDER BY ActionDate) PriorCount
    from #Activity a
) t
where t.ActionDate between '2017-05-29' and '2017-05-30'
order by t.ActionDate desc;

Esa consulta realiza un solo escaneo de los datos, pero realiza una clasificación costosa y calcula la ROW_NUMBER()función para cada fila de la tabla, por lo que parece que hay un trabajo adicional aquí:

mal tipo

Sin embargo, si realmente le gusta ese patrón de código, podría definir un índice para hacerlo más eficiente:

CREATE NONCLUSTERED INDEX [IDX_my_nme] ON #Activity
([Address], [ActionDate]) INCLUDE (FILLER);

Eso mueve el tipo hacia el final, que será mucho menos costoso:

buen tipo

Si nada de esto ayuda, entonces necesitará agregar más información a la pregunta, preferiblemente incluyendo planes de ejecución reales.


1
El carrete de índice que encontró fue el problema. Una vez que agregué una nueva nonclustered index [xyz] on [Activity] (Address) include (ActionDate), los tiempos de consulta disminuyeron de más de un minuto a menos de un segundo. +10 si pudiera. ¡Gracias!
Metro Pitufo
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.