Obtener un escaneo aunque espero una búsqueda

9

Necesito optimizar una SELECTdeclaración, pero SQL Server siempre realiza una exploración de índice en lugar de una búsqueda. Esta es la consulta que, por supuesto, se encuentra en un procedimiento almacenado:

CREATE PROCEDURE dbo.something
  @Status INT = NULL,
  @IsUserGotAnActiveDirectoryUser BIT = NULL    
AS

    SELECT [IdNumber], [Code], [Status], [Sex], 
           [FirstName], [LastName], [Profession], 
           [BirthDate], [HireDate], [ActiveDirectoryUser]
    FROM Employee
    WHERE (@Status IS NULL OR [Status] = @Status)
    AND 
    (
      @IsUserGotAnActiveDirectoryUser IS NULL 
      OR 
      (
        @IsUserGotAnActiveDirectoryUser IS NOT NULL AND       
        (
          @IsUserGotAnActiveDirectoryUser = 1 AND ActiveDirectoryUser <> ''
        )
        OR
        (
          @IsUserGotAnActiveDirectoryUser = 0 AND ActiveDirectoryUser = ''
        )
      )
    )

Y este es el índice:

CREATE INDEX not_relevent ON dbo.Employee
(
    [Status] DESC,
    [ActiveDirectoryUser] ASC
)
INCLUDE (...all the other columns in the table...); 

El plan:

Plan de imagen

¿Por qué SQL Server eligió un escaneo? ¿Cómo puedo arreglarlo?

Definiciones de columna:

[Status] int NOT NULL
[ActiveDirectoryUser] VARCHAR(50) NOT NULL

Los parámetros de estado pueden ser:

NULL: all status,
1: Status= 1 (Active employees)
2: Status = 2 (Inactive employees)

IsUserGotAnActiveDirectoryUser puede ser:

NULL: All employees
0: ActiveDirectoryUser is empty for that employee
1: ActiveDirectoryUser  got a valid value (not null and not empty)
Bestter
fuente
¿Puede publicar el plan de ejecución real en alguna parte (no una imagen del mismo, sino el archivo .sqlplan en formato XML)? Supongo que modificó el procedimiento, pero en realidad no obtuvo una nueva compilación a nivel de declaración. ¿Puede cambiar algún texto de la consulta (como agregar el prefijo de esquema al nombre de la tabla ) y luego pasar un valor válido para @Status?
Aaron Bertrand
1
También la definición de índice plantea la pregunta: ¿por qué está activada la clave Status DESC? ¿Para cuántos valores hay Status, para qué sirven (si el número es pequeño), y cada valor se representa aproximadamente por igual? SELECT TOP (20) [Status], c = COUNT(*) FROM dbo.Employee GROUP BY [Status] ORDER BY c DESC;
Aaron Bertrand

Respuestas:

11

No creo que el escaneo sea causado por una búsqueda de una cadena vacía (y si bien podría agregar un índice filtrado para ese caso, solo ayudará a variaciones muy específicas de la consulta). Es más probable que sea víctima de la detección de parámetros y un plan único que no esté optimizado para todas las diversas combinaciones de parámetros (y valores de parámetros) que proporcionará a esta consulta.

Yo llamo a esto el procedimiento de "fregadero de la cocina" , porque espera que una consulta proporcione todas las cosas, incluido el fregadero de la cocina.

Tengo un video sobre mi solución para esto aquí , pero esencialmente, la mejor experiencia que tengo para tales consultas es:

  • Cree la declaración dinámicamente : esto le permitirá omitir cláusulas que mencionan columnas para las que no se proporcionaron parámetros, y garantiza que tendrá un plan optimizado precisamente para los parámetros reales que se pasaron con valores.
  • UsoOPTION (RECOMPILE) : esto evita que valores de parámetros específicos obliguen al tipo de plan incorrecto, especialmente útil cuando hay sesgo de datos, estadísticas erróneas o cuando la primera ejecución de una declaración usa un valor atípico que conducirá a un plan diferente que más tarde y más frecuente ejecuciones
  • Use la opción de servidoroptimize for ad hoc workloads : esto evita que las variaciones de consulta que solo se usan una vez contaminen la memoria caché de su plan.

Habilite la optimización para cargas de trabajo ad hoc:

EXEC sys.sp_configure 'show advanced options', 1;
GO
RECONFIGURE WITH OVERRIDE;
GO
EXEC sys.sp_configure 'optimize for ad hoc workloads', 1;
GO
RECONFIGURE WITH OVERRIDE;
GO
EXEC sys.sp_configure 'show advanced options', 0;
GO
RECONFIGURE WITH OVERRIDE;

Cambia tu procedimiento:

ALTER PROCEDURE dbo.Whatever
  @Status INT = NULL,
  @IsUserGotAnActiveDirectoryUser BIT = NULL
AS
BEGIN 
  SET NOCOUNT ON;
  DECLARE @sql NVARCHAR(MAX) = N'SELECT [IdNumber], [Code], [Status], 
     [Sex], [FirstName], [LastName], [Profession],
     [BirthDate], [HireDate], [ActiveDirectoryUser]
   FROM dbo.Employee -- please, ALWAYS schema prefix
   WHERE 1 = 1';

   IF @Status IS NOT NULL
     SET @sql += N' AND ([Status]=@Status)'

   IF @IsUserGotAnActiveDirectoryUser = 1
     SET @sql += N' AND ActiveDirectoryUser <> ''''';
   IF @IsUserGotAnActiveDirectoryUser = 0
     SET @sql += N' AND ActiveDirectoryUser = ''''';

   SET @sql += N' OPTION (RECOMPILE);';

   EXEC sys.sp_executesql @sql, N'@Status INT, @Status;
END
GO

Una vez que tenga una carga de trabajo basada en ese conjunto de consultas que puede monitorear, puede analizar las ejecuciones y ver cuáles se beneficiarían más de índices adicionales o diferentes: puede hacerlo desde una variedad de ángulos, desde una simple "combinación de los parámetros se proporcionan con mayor frecuencia? " a "¿qué consultas individuales tienen los tiempos de ejecución más largos?" No podemos responder a esas preguntas basándonos solo en su código, solo podemos sugerir que es NULL, luego no es posible buscar ese índice no agrupado. Entonces, para aquellos casos en los que a los usuarios no les importa el estado, obtendrá un escaneo, a menos que tenga un índice que atienda a las otras cláusulas (pero dicho índice tampoco será útil, dada su lógica de consulta actual - una cadena vacía o no vacía no es exactamente selectiva). cualquier índice solo será útil para un subconjunto de todas las combinaciones de parámetros posibles que está intentando admitir. Por ejemplo, si@Status

En este caso, dependiendo del conjunto de Statusvalores posibles y de la distribución de esos valores, OPTION (RECOMPILE)puede que no sea necesario. Pero si tiene algunos valores que arrojarán 100 filas y algunos valores que arrojarán cientos de miles, es posible que lo desee allí (incluso al costo de la CPU, que debería ser marginal dada la complejidad de esta consulta), para que pueda get busca en la mayor cantidad de casos posible. Si el rango de valores es lo suficientemente finito, incluso podría hacer algo complicado con el SQL dinámico, donde dice "Tengo este valor muy selectivo para @Status, así que cuando se pasa ese valor específico, haga esta ligera alteración en el texto de la consulta para que esto se considera una consulta diferente y está optimizado para ese valor de parámetro ".

Aaron Bertrand
fuente
3
He usado este enfoque muchas veces y es una forma fantástica de hacer que el optimizador haga las cosas de la manera que crees que debería hacerlo de todos modos. Kim Tripp habla sobre una solución similar aquí: sqlskills.com/blogs/kimberly/high-performance-procedures Y tiene un video de una sesión que hizo en PASS hace un par de años que realmente entra en detalles locos de por qué funciona. Eso dice, realmente no agrega mucho a lo que el Sr. Bertrand dijo aquí. Esta es una de esas herramientas que todos deberían tener en su cinturón de herramientas. Realmente puede ahorrar algunos dolores masivos para esas consultas generales.
mskinner
3

Descargo de responsabilidad : algunas de las cosas en esta respuesta pueden hacer que un DBA retroceda. Lo estoy abordando desde un punto de vista de rendimiento puro: cómo obtener Index Seeks cuando siempre obtiene Index Scans.

Con eso fuera del camino, aquí va.

Su consulta es lo que se conoce como "consulta del fregadero de la cocina", una consulta única destinada a satisfacer un rango de posibles condiciones de búsqueda. Si el usuario establece @statusun valor, desea filtrar ese estado. Si @statuses así NULL, devuelve todos los estados, y así sucesivamente.

Esto introduce problemas con la indexación, pero no están relacionados con la sargabilidad, porque todas las condiciones de búsqueda son "iguales" a los criterios.

Esto es sargable:

WHERE [status]=@status

Esto no es modificable porque SQL Server necesita evaluar ISNULL([status], 0)cada fila en lugar de buscar un solo valor en el índice:

WHERE ISNULL([status], 0)=@status

He recreado el problema del fregadero de la cocina de una forma más simple:

CREATE TABLE #work (
    A    int NOT NULL,
    B    int NOT NULL
);

CREATE UNIQUE INDEX #work_ix1 ON #work (A, B);

INSERT INTO #work (A, B)
VALUES (1,  1), (2,  1),
       (3,  1), (4,  1),
       (5,  2), (6,  2),
       (7,  2), (8,  3),
       (9,  3), (10, 3);

Si intenta lo siguiente, obtendrá un Escaneo de índice, aunque A es la primera columna del índice:

DECLARE @a int=4, @b int=NULL;

SELECT *
FROM #work
WHERE (@a IS NULL OR @a=A) AND
      (@b IS NULL OR @b=B);

Esto, sin embargo, produce una búsqueda de índice:

DECLARE @a int=4, @b int=NULL;

SELECT *
FROM #work
WHERE @a=A AND
      @b IS NULL;

Siempre que esté utilizando una cantidad manejable de parámetros (dos en su caso), probablemente podría realizar UNIONun montón de consultas de búsqueda, básicamente todas las permutaciones de los criterios de búsqueda. Si tiene tres criterios, esto se verá desordenado, con cuatro será completamente inmanejable. Has sido advertido.

DECLARE @a int=4, @b int=NULL;

SELECT *
FROM #work
WHERE @a=A AND
      @b IS NULL
UNION ALL
SELECT *
FROM #work
WHERE @a=A AND
      @b=B
UNION ALL
SELECT *
FROM #work
WHERE @a IS NULL AND
      @b=B
UNION ALL
SELECT *
FROM #work
WHERE @a IS NULL AND
      @b IS NULL;

Sin (B, A)embargo, para que el tercero de esos cuatro use un Index Seek, necesitará un segundo índice . Así es como se vería su consulta con estos cambios (incluida mi refactorización de la consulta para hacerla más legible).

DECLARE @Status int = NULL,
        @IsUserGotAnActiveDirectoryUser bit = NULL;

SELECT [IdNumber], [Code], [Status], [Sex], [FirstName], [LastName],
       [Profession], [BirthDate], [HireDate], [ActiveDirectoryUser]
FROM Employee
WHERE [Status]=@Status AND
      @IsUserGotAnActiveDirectoryUser IS NULL

UNION ALL

SELECT [IdNumber], [Code], [Status], [Sex], [FirstName], [LastName],
       [Profession], [BirthDate], [HireDate], [ActiveDirectoryUser]
FROM Employee
WHERE [Status]=@Status AND
      @IsUserGotAnActiveDirectoryUser=1 AND ActiveDirectoryUser<>''

UNION ALL

SELECT [IdNumber], [Code], [Status], [Sex], [FirstName], [LastName],
       [Profession], [BirthDate], [HireDate], [ActiveDirectoryUser]
FROM Employee
WHERE [Status]=@Status AND
      @IsUserGotAnActiveDirectoryUser=0 AND (ActiveDirectoryUser IS NULL OR ActiveDirectoryUser='')

UNION ALL

SELECT [IdNumber], [Code], [Status], [Sex], [FirstName], [LastName],
       [Profession], [BirthDate], [HireDate], [ActiveDirectoryUser]
FROM Employee
WHERE @Status IS NULL AND
      @IsUserGotAnActiveDirectoryUser IS NULL

UNION ALL

SELECT [IdNumber], [Code], [Status], [Sex], [FirstName], [LastName],
       [Profession], [BirthDate], [HireDate], [ActiveDirectoryUser]
FROM Employee
WHERE @Status IS NULL AND
      @IsUserGotAnActiveDirectoryUser=1 AND ActiveDirectoryUser<>''

UNION ALL

SELECT [IdNumber], [Code], [Status], [Sex], [FirstName], [LastName],
       [Profession], [BirthDate], [HireDate], [ActiveDirectoryUser]
FROM Employee
WHERE @Status IS NULL AND
      @IsUserGotAnActiveDirectoryUser=0 AND (ActiveDirectoryUser IS NULL OR ActiveDirectoryUser='');

... además, necesitará un índice adicional Employeecon las dos columnas de índice invertidas.

Para completar, debo mencionar que x=@ximplícitamente significa que xno puede ser NULLporque NULLnunca es igual a NULL. Eso simplifica un poco la consulta.

Y sí, la respuesta dinámica de SQL de Aaron Bertrand es una mejor opción en la mayoría de los casos (es decir, siempre que pueda vivir con las recompilaciones).

Daniel Hutmacher
fuente
3

Su pregunta básica parece ser "Por qué" y creo que puede encontrar la respuesta sobre el minuto 55 más o menos de esta gran presentación de Adam Machanic en TechEd hace unos años.

Menciono los 5 minutos en el minuto 55, pero toda la presentación vale la pena. Si observa el plan de consulta para su consulta, estoy seguro de que encontrará que tiene predicados residuales para la búsqueda. Básicamente, SQL no puede "ver" todas las partes del índice porque algunas de ellas están ocultas por las desigualdades y otras condiciones. El resultado es una exploración de índice para un superconjunto basado en el predicado. Ese resultado se pone en cola y luego se vuelve a escanear utilizando el predicado residual.

Verifique las propiedades del Operador de escaneo (F4) y vea si tiene tanto "Buscar predicado" como "Predicar" en la lista de propiedades.

Como otros han indicado, la consulta es difícil de indexar tal como está. He estado trabajando en muchos similares recientemente y cada uno ha requerido una solución diferente. :(

Rayo
fuente
0

Antes de preguntarnos si se prefiere la búsqueda de índice sobre la exploración de índice, una regla general es verificar cuántas filas se devuelven frente a las filas totales de la tabla subyacente. Por ejemplo, si espera que su consulta devuelva 10 filas de 1 millón de filas, entonces la búsqueda de índice probablemente sea más preferible que la exploración de índice. Sin embargo, si se van a devolver unos pocos miles de filas (o más) de la consulta, entonces la búsqueda de índice NO necesariamente se preferirá.

Su consulta no es compleja, por lo que si puede publicar un plan de ejecución, podemos tener mejores ideas para ayudarlo.

jyao
fuente
Al filtrar algunos miles de filas de una tabla de 1 millón, todavía me gustaría una búsqueda: sigue siendo una gran mejora del rendimiento en comparación con el escaneo de toda la tabla.
Daniel Hutmacher
-6

este es solo el formato original

DECLARE @Status INT = NULL,
        @IsUserGotAnActiveDirectoryUser BIT = NULL    

SELECT [IdNumber], [Code], [Status], [Sex], [FirstName], [LastName], [Profession],
       [BirthDate], [HireDate], [ActiveDirectoryUser]
FROM Employee
WHERE (@Status IS NULL OR [Status]=@Status)  
AND (            @IsUserGotAnActiveDirectoryUser IS NULL 
      OR (       @IsUserGotAnActiveDirectoryUser IS NOT NULL 
           AND (     @IsUserGotAnActiveDirectoryUser = 1 
                 AND ActiveDirectoryUser <> '') 
           OR  (     @IsUserGotAnActiveDirectoryUser = 0 
                 AND ActiveDirectoryUser =  '')
         )
    )

Esta es la revisión: no estoy 100% seguro, pero (tal vez) lo intente,
incluso uno O probablemente sea un problema que
se rompería en ActiveDirectoryUser nulo

  WHERE isnull(@Status, [Status]) = [Status]
    AND (      (     isnull(@IsUserGotAnActiveDirectoryUser, 1) = 1 
                 AND ActiveDirectoryUser <> '' ) 
           OR  (     isnull(@IsUserGotAnActiveDirectoryUser, 0) = 0 
                 AND ActiveDirectoryUser =  '' )
        )
paparazzo
fuente
3
No me queda claro cómo esta respuesta resuelve la pregunta del OP.
Erik
@Erik ¿Podríamos dejar que el OP lo pruebe? Dos OR se fueron. ¿Sabe con certeza que esto no puede ayudar al rendimiento de la consulta?
paparazzo
@ ypercubeᵀᴹ IsUserGotAnActiveDirectoryUser IS NOT NULL se elimina. Esos dos innecesarios eliminan un OR y eliminan IsUserGotAnActiveDirectoryUser IS NULL. ¿Está seguro de que esta consulta no se ejecutará rápidamente, entonces el OP?
paparazzo
@ ypercubeᵀᴹ Podría haber hecho muchas cosas. No estoy buscando más simple. Dos o se han ido. O suele ser malo para los planes de consulta. Llego allí, es una especie de club y no soy parte del club. Pero hago esto para vivir y publico lo que sé que ha funcionado. Mis respuestas no se ven afectadas por los votos negativos.
paparazzo