¿Por qué es esto más rápido y seguro de usar? (DONDE la primera letra está en el alfabeto)

10

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

JohnF
fuente
44
Pequeña respuesta a su nota técnica: AND LEFT(TRIM(largeTbl.last_name), 1) BETWEEN 'a' AND 'z' COLLATE LATIN1_GENERAL_CI_AIdebe hacer lo que quiera allí sin requerir que enumere todos los caracteres y tenga un código que sea difícil de leer
Erik A
¿Tiene filas donde la condición final en el WHEREes falsa? En particular, tenga en cuenta que la comparación puede ser sensible a mayúsculas y minúsculas.
jpmc26
@ErikvonAsmuth hace un excelente punto. Pero, solo una pequeña nota técnica: para SQL Server 2008 y 2008 R2, es mejor usar las colaciones de la versión "100" (si están disponibles para la cultura / localidad que se usa). Entonces eso sería 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ía Latin1_General_100_CI_AI_SCen este caso. Las versiones> 100 (solo japonesas hasta ahora) no tienen (o necesitan) _SC(por ejemplo Japanese_XJIS_140_CI_AI).
Solomon Rutzky

Respuestas:

9

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" ingrese la descripción de la imagen aquí

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" ingrese la descripción de la imagen aquí

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. ingrese la descripción de la imagen aquí

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.

ingrese la descripción de la imagen aquí ingrese la descripción de la imagen aquí

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:

ingrese la descripción de la imagen aquí

En lugar de una unión de bucle anidado en el no optimizado:

ingrese la descripción de la imagen aquí

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 ingrese la descripción de la imagen aquí

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 ingrese la descripción de la imagen aquí 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.

  • Actualización de estadísticas, usando recompilar para tratar de obtener el mejor plan.
  • Agregar OPCIÓN (HASH JOIN, MERGE JOIN)a la consulta
  • ...

Datos de prueba + consultas utilizadas

CREATE TABLE #smallTableOfPeople(importantValue int, birthDate datetime2, first_name varchar(50),last_name varchar(50));
CREATE TABLE #largeTableOfPeople(importantValue int, birth_date datetime2, first_name varchar(50),last_name varchar(50));


set nocount on;
DECLARE @i int = 1
WHILE @i <= 1000
BEGIN
insert into #smallTableOfPeople (importantValue,birthDate,first_name,last_name)
VALUES(NULL, dateadd(mi,@i,'2018-01-18 11:05:29.067'),'Frodo','Baggins');

set @i += 1;
END


set nocount on;
DECLARE @j int = 1
WHILE @j <= 20000
BEGIN
insert into #largeTableOfPeople (importantValue,birth_Date,first_name,last_name)
VALUES(@j, dateadd(mi,@j,'2018-01-18 11:05:29.067'),'Frodo','Baggins');

set @j += 1;
END


SET STATISTICS IO, TIME ON;

SELECT  smallTbl.importantValue , largeTbl.importantValue
FROM #smallTableOfPeople smallTbl
JOIN #largeTableOfPeople largeTbl
    ON largeTbl.birth_date = smallTbl.birthDate
    AND DIFFERENCE(RTRIM(LTRIM(smallTbl.last_name)),RTRIM(LTRIM(largeTbl.last_name))) = 4
    AND DIFFERENCE(RTRIM(LTRIM(smallTbl.first_name)),RTRIM(LTRIM(largeTbl.first_name))) = 4
WHERE smallTbl.importantValue IS NULL
-- The following line is "the optimization"
AND LEFT(RTRIM(LTRIM(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','æ','ä','ø','å');

SELECT  smallTbl.importantValue , largeTbl.importantValue
FROM #smallTableOfPeople smallTbl
JOIN #largeTableOfPeople largeTbl
    ON largeTbl.birth_date = smallTbl.birthDate
    AND DIFFERENCE(RTRIM(LTRIM(smallTbl.last_name)),RTRIM(LTRIM(largeTbl.last_name))) = 4
    AND DIFFERENCE(RTRIM(LTRIM(smallTbl.first_name)),RTRIM(LTRIM(largeTbl.first_name))) = 4
WHERE smallTbl.importantValue IS NULL
-- The following line is "the optimization"
--AND LEFT(RTRIM(LTRIM(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','æ','ä','ø','å')




drop table #largeTableOfPeople;
drop table #smallTableOfPeople;
Randi Vertongen
fuente
8

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 de 190,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

ingrese la descripción de la imagen aquí

Segundo plan

ingrese la descripción de la imagen aquí

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 INcondiciones solo eliminó 51 filas de las 4,008,334 en la tabla, pero el optimizador consideró que eliminaría mucho más

ingrese la descripción de la imagen aquí

   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', 'æ', 'ä', 'ø', 'å' ) 

Tales 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 TRIMSQL 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 el TRIMsolo 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 es table_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 si varchar(100)se rellena con espacios finales, como sucede con charla estimación más baja, se utiliza

La INlista se expande ORy SQL Server usa un retroceso exponencial con un máximo de 4 predicados considerados. Entonces 219.707se llega a la estimación de la siguiente manera.

DECLARE @TableCardinality FLOAT = 4008334, 
        @DistinctColumnValueEstimate FLOAT = 34207

DECLARE @NotSelectivity float = 1 - (1/@DistinctColumnValueEstimate)

SELECT @TableCardinality * ( 1 - (
@NotSelectivity * 
SQRT(@NotSelectivity) * 
SQRT(SQRT(@NotSelectivity)) * 
SQRT(SQRT(SQRT(@NotSelectivity)))
))
Martin Smith
fuente