Enorme desaceleración de la consulta de SQL Server al agregar comodines (o arriba)

52

Tengo un zoológico de 20 millones de animales que rastreé en mi base de datos SQL Server 2005. Aproximadamente el 1% de ellos son negros y aproximadamente el 1% de ellos son cisnes. Quería obtener detalles de todos los cisnes negros y, por lo tanto, no quería saturar la página de resultados.

select top 10 * 
from animal 
where colour like 'black'  
and species like 'swan'

(Sí, desafortunadamente esos campos son de texto libre pero ambos están indexados). Resulta que no tenemos tales animales, ya que la consulta devuelve un conjunto vacío en aproximadamente 300 milisegundos. Hubiera sido aproximadamente el doble de rápido si hubiera usado '=' en lugar de 'me gusta', pero tengo una premonición de que este último está a punto de ahorrarme algo de tipeo.

Resulta que el director del zoológico cree que puede haber ingresado a algunos de los cisnes como 'negruzcos', por lo que modifico la consulta en consecuencia:

select top 10 * 
from animal  
where colour like 'black%' 
and species like 'swan'

Resulta que tampoco hay ninguno de esos (y, de hecho, no hay animales 'negros' excepto los 'negros'), pero la consulta ahora tarda unos 30 segundos en volver vacía.

Parece que solo la combinación de 'top' y 'like%' está causando problemas porque

select count(*) 
from animal  
where colour like 'black%' 
and species like 'swan'

devuelve 0 muy rápidamente, e incluso

select * 
from animal 
where colour like 'black%' 
and species like 'swan'

vuelve vacío en una fracción de segundo.

¿Alguien tiene alguna idea de por qué 'top' y '%' deberían conspirar para causar una pérdida de rendimiento tan dramática, especialmente en un conjunto de resultados vacío?

EDITAR: Solo para aclarar, no estoy usando ningún índice de FreeText, solo quería decir que los campos son de texto libre en el punto de entrada, es decir, no están normalizados en la base de datos. Perdón por la confusión, mala redacción de mi parte.

Stovroz
fuente

Respuestas:

76

Esta es una decisión del optimizador basado en costos.

Los costos estimados utilizados en esta elección son incorrectos, ya que supone independencia estadística entre los valores en diferentes columnas.

Es similar al problema descrito en Row Goals Gone Rogue donde los números pares e impares están correlacionados negativamente.

Es fácil de reproducir.

CREATE TABLE dbo.animal(
    id int IDENTITY(1,1) NOT NULL PRIMARY KEY,
    colour varchar(50) NOT NULL,
    species varchar(50) NOT NULL,
    Filler char(10) NULL
);

/*Insert 20 million rows with 1% black and 1% swan but no black swans*/
WITH T
     AS (SELECT TOP 20000000 ROW_NUMBER() OVER (ORDER BY @@SPID) AS RN
         FROM   master..spt_values v1,
                master..spt_values v2,
                master..spt_values v3)
INSERT INTO dbo.animal
            (colour,
             species)
SELECT CASE
         WHEN RN % 100 = 1 THEN 'black'
         ELSE CAST(RN % 100 AS VARCHAR(3))
       END,
       CASE
         WHEN RN % 100 = 2 THEN 'swan'
         ELSE CAST(RN % 100 AS VARCHAR(3))
       END
FROM   T 

/*Create some indexes*/
CREATE NONCLUSTERED INDEX ix_species ON  dbo.animal(species);
CREATE NONCLUSTERED INDEX ix_colour ON  dbo.animal(colour);

Ahora intenta

SELECT TOP 10 *
FROM   animal
WHERE  colour LIKE 'black'
       AND species LIKE 'swan' 

Esto le da el siguiente plan que tiene un costo 0.0563167.

ingrese la descripción de la imagen aquí

El plan puede realizar una combinación de fusión entre los resultados de los dos índices en la idcolumna. ( Más detalles del algoritmo de combinación de combinación aquí ).

La combinación de combinación requiere que ambas entradas sean ordenadas por la clave de combinación.

Los índices no agrupados se ordenan por (species, id)y (colour, id)respectivamente (los índices no agrupados no únicos siempre tienen el localizador de fila agregado al final de la clave implícitamente si no se agrega explícitamente). La consulta sin comodines está realizando una búsqueda de igualdad en species = 'swan'y colour ='black'. Como cada búsqueda solo recupera un valor exacto de la columna inicial, las filas coincidentes se ordenarán por idlo que este plan es posible.

Los operadores del plan de consulta se ejecutan de izquierda a derecha . Con el operador izquierdo solicitando filas de sus elementos secundarios, que a su vez solicitan filas de sus elementos secundarios (y así sucesivamente hasta llegar a los nodos hoja). El TOPiterador dejará de solicitar más filas de su hijo una vez que se hayan recibido 10.

SQL Server tiene estadísticas sobre los índices que le indican que el 1% de las filas coinciden con cada predicado. Se supone que estas estadísticas son independientes (es decir, no están correlacionadas ni positiva ni negativamente) de modo que, en promedio, una vez que ha procesado 1,000 filas que coinciden con el primer predicado, encontrará 10 que coinciden con el segundo y puede salir. (el plan anterior en realidad muestra 987 en lugar de 1,000 pero lo suficientemente cerca).

De hecho, como los predicados están correlacionados negativamente, el plan real muestra que todas las 200,000 filas coincidentes debían procesarse a partir de cada índice, pero esto se mitiga en cierta medida porque las filas unidas cero también significa que realmente se necesitaban cero búsquedas.

Comparar con

SELECT TOP 10 *
FROM   animal
WHERE  colour LIKE 'black%'
       AND species LIKE 'swan' 

Lo que da el siguiente plan que tiene un costo de 0.567943

ingrese la descripción de la imagen aquí

La adición del comodín final ahora ha provocado un escaneo de índice. Sin embargo, el costo del plan sigue siendo bastante bajo para una exploración en una tabla de 20 millones de filas.

Agregar querytraceon 9130muestra más información

SELECT TOP 10 *
FROM   animal
WHERE  colour LIKE 'black%'
       AND species LIKE 'swan'       
OPTION (QUERYTRACEON 9130)  

ingrese la descripción de la imagen aquí

Se puede ver que SQL Server reconoce que solo necesitará escanear alrededor de 100,000 filas antes de encontrar 10 que coincidan con el predicado y TOPpueda dejar de solicitar filas.

Nuevamente, esto tiene sentido con el supuesto de independencia como 10 * 100 * 100 = 100,000

Finalmente intentemos forzar un plan de intersección de índice

SELECT TOP 10 *
FROM   animal WITH (INDEX(ix_species), INDEX(ix_colour))
WHERE  colour LIKE 'black%'
       AND species LIKE 'swan' 

Esto me da un plan paralelo con un costo estimado de 3.4625

ingrese la descripción de la imagen aquí

La principal diferencia aquí es que el colour like 'black%'predicado ahora puede coincidir con varios colores diferentes. Esto significa que ya no se garantiza que las filas de índice coincidentes para ese predicado se ordenen en orden de id.

Por ejemplo, la búsqueda de índice like 'black%'podría devolver las siguientes filas

+------------+----+
|   Colour   | id |
+------------+----+
| black      | 12 |
| black      | 20 |
| black      | 23 |
| black      | 25 |
| blackberry |  1 |
| blackberry | 50 |
+------------+----+

Dentro de cada color, los identificadores están ordenados, pero los identificadores en diferentes colores pueden no estarlo.

Como resultado, SQL Server ya no puede realizar una intersección de índice de combinación de combinación (sin agregar un operador de clasificación de bloqueo) y opta por realizar una combinación hash. Hash Join está bloqueando la entrada de compilación, por lo que ahora el costo refleja el hecho de que todas las filas coincidentes deberán procesarse a partir de la entrada de compilación en lugar de suponer que solo tendrá que escanear 1,000 como en el primer plan.

Sin embargo, la entrada de la sonda no está bloqueando y todavía estima incorrectamente que podrá detener el sondeo después de procesar 987 filas a partir de eso.

(Aquí encontrará más información sobre iteradores sin bloqueo frente a iteradores de bloqueo)

Dado el aumento de los costos de las filas adicionales estimadas y la unión de hash, el escaneo de índice agrupado parcial parece más barato.

En la práctica, por supuesto, el escaneo de índice agrupado "parcial" no es parcial en absoluto y necesita atravesar los 20 millones de filas en lugar de los 100 mil supuestos al comparar los planes.

Aumentar el valor de TOP(o eliminarlo por completo) finalmente encuentra un punto de inflexión en el que el número de filas que estima que el escaneo de CI necesitará cubrir hace que ese plan parezca más costoso y vuelve al plan de intersección de índice. Para mí, el punto de corte entre los dos planes es TOP (89)vs TOP (90).

Para usted, puede diferir, ya que depende de qué tan ancho sea el índice agrupado.

Eliminar TOPy forzar el escaneo CI

SELECT *
FROM   animal WITH (INDEX = 1)
WHERE  colour LIKE 'black%'
       AND species LIKE 'swan' 

Cuesta en 88.0586mi máquina para mi tabla de ejemplo.

Si SQL Server supiera que el zoológico no tenía cisnes negros y que necesitaría hacer un análisis completo en lugar de solo leer 100,000 filas, este plan no se elegiría.

He intentado varias columnas estadísticas sobre animal(species,colour)y animal(colour,species)y se filtra en las estadísticas animal (colour) where species = 'swan'pero ninguna de ellas ayuda convencerlo de que no existen cisnes negros y la TOP 10exploración tendrá que procesar más de 100.000 filas.

Esto se debe a la "suposición de inclusión" donde SQL Server esencialmente asume que si está buscando algo, probablemente exista.

En 2008+ hay una marca de seguimiento documentada 4138 que desactiva los objetivos de la fila. El efecto de esto es que el plan tiene un costo sin la suposición de que TOPpermitirá a los operadores secundarios terminar antes de tiempo sin leer todas las filas coincidentes. Con esta marca de seguimiento en su lugar, naturalmente obtengo el plan de intersección de índice más óptimo.

SELECT TOP 10 *
FROM   animal
WHERE  colour LIKE 'black%'
       AND species LIKE 'swan'
OPTION (QUERYTRACEON 4138)       

ingrese la descripción de la imagen aquí

Este plan ahora cuesta correctamente leer las 200 mil filas completas en ambas búsquedas de índice, pero sobrepasa los costos de las búsquedas clave (se estima que 2 mil frente a 0 real TOP 10. . Aún así, el plan tiene un costo significativamente más barato que el análisis de CI completo, por lo que se selecciona.

Por supuesto, este plan podría no ser óptimo para combinaciones que son comunes. Como los cisnes blancos.

Un índice compuesto en animal (colour, species)o idealmente animal (species, colour)permitiría que la consulta sea mucho más eficiente para ambos escenarios.

Para hacer un uso más eficiente del índice compuesto LIKE 'swan', también debería cambiarse a = 'swan'.

La siguiente tabla muestra los predicados de búsqueda y los predicados residuales que se muestran en los planes de ejecución para las cuatro permutaciones.

+----------------------------------------------+-------------------+----------------------------------------------------------------+----------------------------------------------+
|                 WHERE clause                 |       Index       |                         Seek Predicate                         |              Residual Predicate              |
+----------------------------------------------+-------------------+----------------------------------------------------------------+----------------------------------------------+
| colour LIKE 'black%' AND species LIKE 'swan' | ix_colour_species | colour >= 'black' AND colour < 'blacL'                         | colour like 'black%' AND species like 'swan' |
| colour LIKE 'black%' AND species LIKE 'swan' | ix_species_colour | species >= 'swan' AND species <= 'swan'                        | colour like 'black%' AND species like 'swan' |
| colour LIKE 'black%' AND species = 'swan'    | ix_colour_species | (colour,species) >= ('black', 'swan')) AND colour < 'blacL'    | colour LIKE 'black%' AND species = 'swan'    |
| colour LIKE 'black%' AND species = 'swan'    | ix_species_colour | species = 'swan' AND (colour >= 'black' and colour <  'blacL') | colour like 'black%'                         |
+----------------------------------------------+-------------------+----------------------------------------------------------------+----------------------------------------------+
Martin Smith
fuente
15

Al encontrar esto intrigante, busqué un poco y me topé con este Q / A ¿Cómo (y por qué) TOP impacta un plan de ejecución?

Básicamente, el uso de TOP cambia el costo de los operadores que lo siguen (de una manera no trivial), lo que hace que el plan general cambie también (sería genial si incluyera ExecPlans con y sin TOP 10), lo que cambia bastante la ejecución general de la consulta.

Espero que esto ayude.

Por ejemplo, lo probé en una base de datos y: -cuando no se invoca top, se usa paralelismo -con TOP, no se usa paralelismo

Entonces, nuevamente, mostrar sus planes de ejecución proporcionaría más información.

Que tengas un buen día

Vladislav Zalesak
fuente
-1

Creo que esto puede deberse a la naturaleza subyacente de MSSQL 2005 y la forma en que el optimizador de consultas decide qué plan de ejecución es el más eficiente.

Si usa una variable SQL, debe 'engañar' al optimizador de consultas para que use coincidencias hash en lugar de bucles anidados, lo que dará como resultado un grado de paralelismo mucho mayor.

Tratar:

DECLARE @topn INT = 10
SELECT TOP (@topn) *
FROM    animal
WHERE   colour LIKE 'black%' 
AND species LIKE 'swan'

fuente
55
Ofuscar el TOPvalor en una variable significa que asumirá en TOP 100lugar de TOP 10. Esto puede o no ayudar dependiendo del punto de inflexión entre los dos planes.
Martin Smith