En pocas palabras, estamos actualizando pequeñas tablas de personas con valores de una tabla muy grande de personas. En una prueba reciente, esta actualización tarda unos 5 minutos en ejecutarse.
Nos topamos con lo que parece la optimización más tonta posible, ¡que aparentemente funciona perfectamente! La misma consulta ahora se ejecuta en menos de 2 minutos y produce los mismos resultados, perfectamente.
Aquí está la consulta. La última línea se agrega como "la optimización". ¿Por qué la disminución intensa en el tiempo de consulta? ¿Nos estamos perdiendo algo? ¿Podría esto conducir a problemas en el futuro?
UPDATE smallTbl
SET smallTbl.importantValue = largeTbl.importantValue
FROM smallTableOfPeople smallTbl
JOIN largeTableOfPeople largeTbl
ON largeTbl.birth_date = smallTbl.birthDate
AND DIFFERENCE(TRIM(smallTbl.last_name),TRIM(largeTbl.last_name)) = 4
AND DIFFERENCE(TRIM(smallTbl.first_name),TRIM(largeTbl.first_name)) = 4
WHERE smallTbl.importantValue IS NULL
-- The following line is "the optimization"
AND LEFT(TRIM(largeTbl.last_name), 1) IN ('a','à','á','b','c','d','e','è','é','f','g','h','i','j','k','l','m','n','o','ô','ö','p','q','r','s','t','u','ü','v','w','x','y','z','æ','ä','ø','å')
Notas técnicas: Somos conscientes de que la lista de letras para probar puede necesitar algunas letras más. También conocemos el margen obvio de error al usar "DIFERENCIA".
Plan de consulta (regular): https://www.brentozar.com/pastetheplan/?id=rypV84y7V
Plan de consulta (con "optimización"): https://www.brentozar.com/pastetheplan/?id=r1aC2my7E
AND LEFT(TRIM(largeTbl.last_name), 1) BETWEEN 'a' AND 'z' COLLATE LATIN1_GENERAL_CI_AI
debe hacer lo que quiera allí sin requerir que enumere todos los caracteres y tenga un código que sea difícil de leerWHERE
es falsa? En particular, tenga en cuenta que la comparación puede ser sensible a mayúsculas y minúsculas.Latin1_General_100_CI_AI
. Y para SQL Server 2012 y versiones más recientes (a través de al menos SQL Server 2019), es mejor usar las intercalaciones con caracteres complementarios habilitados en la versión más alta para la configuración regional que se utiliza. Entonces eso seríaLatin1_General_100_CI_AI_SC
en este caso. Las versiones> 100 (solo japonesas hasta ahora) no tienen (o necesitan)_SC
(por ejemploJapanese_XJIS_140_CI_AI
).Respuestas:
Depende de los datos en sus tablas, sus índices, ... Difícil de decir sin poder comparar los planes de ejecución / las estadísticas io + time.
La diferencia que esperaría es el filtrado adicional que ocurre antes de la UNIÓN entre las dos tablas. En mi ejemplo, cambié las actualizaciones a selecciones para reutilizar mis tablas.
El plan de ejecución con "la optimización"
Plan de ejecución
Usted ve claramente que ocurre una operación de filtro, en mis datos de prueba no se registraron registros y, como resultado, no se realizaron mejoras.
El plan de ejecución, sin "la optimización"
Plan de ejecución
El filtro se ha ido, lo que significa que tendremos que confiar en la unión para filtrar los registros innecesarios.
Otra (s) razón (es) Otra razón / consecuencia de cambiar la consulta podría ser, que se creó un nuevo plan de ejecución al cambiar la consulta, que resulta ser más rápido. Un ejemplo de esto es el motor que elige un operador de Unión diferente, pero eso es solo adivinar en este punto.
EDITAR:
Aclarando después de obtener los dos planes de consulta:
La consulta lee 550 millones de filas de la tabla grande y las filtra.
Lo que significa que el predicado es el que realiza la mayor parte del filtrado, no el predicado de búsqueda. Como resultado, los datos se leen, pero mucho menos se devuelven.
Hacer que el servidor sql use un índice diferente (plan de consulta) / agregar un índice podría resolver esto.
Entonces, ¿por qué la consulta de optimización no tiene el mismo problema?
Porque se utiliza un plan de consulta diferente, con un escaneo en lugar de una búsqueda.
Sin hacer ninguna búsqueda, pero solo devolviendo 4M filas para trabajar.
Siguiente diferencia
Sin tener en cuenta la diferencia de actualización (no se actualiza nada en la consulta optimizada), se utiliza una coincidencia hash en la consulta optimizada:
En lugar de una unión de bucle anidado en el no optimizado:
Un bucle anidado es mejor cuando una tabla es pequeña y la otra grande. Como ambos están cerca del mismo tamaño, diría que la coincidencia de hash es la mejor opción en este caso.
Visión de conjunto
La consulta optimizada
El plan de la consulta optimizada tiene paralelismo, utiliza una combinación de coincidencia hash y necesita hacer menos filtrado de E / S residual. También usa un mapa de bits para eliminar los valores clave que no pueden producir filas de unión. (Tampoco se actualiza nada)
La consulta no optimizada El plan de la consulta no optimizada no tiene paralelismo, utiliza una unión de bucle anidado y necesita hacer un filtrado de E / S residual en los registros de 550M. (También está ocurriendo la actualización)
¿Qué podría hacer para mejorar la consulta no optimizada?
Cambiar el índice para tener nombre_nombre & apellido_en la lista de columnas clave:
CREATE INDEX IX_largeTableOfPeople_birth_date_first_name_last_name en dbo.largeTableOfPeople (birth_date, first_name, last_name) include (id)
Pero debido al uso de funciones y a que esta tabla es grande, esta podría no ser la solución óptima.
(HASH JOIN, MERGE JOIN)
a la consultaDatos de prueba + consultas utilizadas
fuente
No está claro que la segunda consulta sea, de hecho, una mejora.
Los planes de ejecución contienen QueryTimeStats que muestran una diferencia mucho menos dramática que la indicada en la pregunta.
El plan lento tuvo un tiempo transcurrido de
257,556 ms
(4 minutos y 17 segundos). El plan rápido tuvo un tiempo transcurrido de190,992 ms
(3 minutos y 11 segundos) a pesar de correr con un grado de paralelismo de 3.Además, el segundo plan se estaba ejecutando en una base de datos donde no había trabajo que hacer después de la unión.
Primer plan
Segundo plan
De modo que ese tiempo adicional bien podría explicarse por el trabajo necesario para actualizar 3.5 millones de filas (el trabajo requerido en el operador de actualización para ubicar estas filas, bloquear la página, escribir la actualización en la página y el registro de transacciones no es insignificante)
Si esto es de hecho reproducible al comparar like con like, entonces la explicación es que usted tuvo suerte en este caso.
El filtro con las 37
IN
condiciones solo eliminó 51 filas de las 4,008,334 en la tabla, pero el optimizador consideró que eliminaría mucho másTales estimaciones incorrectas de cardinalidad suelen ser algo malo. En este caso, produjo un plan de forma diferente (y paralelo) que aparentemente (?) Funcionó mejor para usted a pesar de los derrames de hash causados por la subestimación masiva.
Sin el
TRIM
SQL Server es capaz de convertir esto en un intervalo de rango en el histograma de la columna base y dar estimaciones mucho más precisas, pero con elTRIM
solo recurre a conjeturas.La naturaleza de la suposición puede variar, pero la estimación para un solo predicado
LEFT(TRIM(largeTbl.last_name), 1)
en algunas circunstancias * solo se estima que estable_cardinality/estimated_number_of_distinct_column_values
.No estoy seguro exactamente de qué circunstancias: el tamaño de los datos parece jugar un papel importante. Pude reproducir esto con tipos de datos anchos de longitud fija como aquí, pero obtuve una suposición diferente y más alta
varchar
(que solo usó una suposición plana del 10% y estimó 100,000 filas). @Solomon Rutzky señala que sivarchar(100)
se rellena con espacios finales, como sucede conchar
la estimación más baja, se utilizaLa
IN
lista se expandeOR
y SQL Server usa un retroceso exponencial con un máximo de 4 predicados considerados. Entonces219.707
se llega a la estimación de la siguiente manera.fuente