Tengo una consulta como la siguiente:
DELETE FROM tblFEStatsBrowsers WHERE BrowserID NOT IN (
SELECT DISTINCT BrowserID FROM tblFEStatsPaperHits WITH (NOLOCK) WHERE BrowserID IS NOT NULL
)
tblFEStatsBrowsers tiene 553 filas.
tblFEStatsPaperHits tiene 47.974.301 filas.
tblFEStatsBrowsers:
CREATE TABLE [dbo].[tblFEStatsBrowsers](
[BrowserID] [smallint] IDENTITY(1,1) NOT NULL,
[Browser] [varchar](50) NOT NULL,
[Name] [varchar](40) NOT NULL,
[Version] [varchar](10) NOT NULL,
CONSTRAINT [PK_tblFEStatsBrowsers] PRIMARY KEY CLUSTERED ([BrowserID] ASC)
)
tblFEStatsPaperHits:
CREATE TABLE [dbo].[tblFEStatsPaperHits](
[PaperID] [int] NOT NULL,
[Created] [smalldatetime] NOT NULL,
[IP] [binary](4) NULL,
[PlatformID] [tinyint] NULL,
[BrowserID] [smallint] NULL,
[ReferrerID] [int] NULL,
[UserLanguage] [char](2) NULL
)
Hay un índice agrupado en tblFEStatsPaperHits que no incluye BrowserID. Por lo tanto, realizar la consulta interna requerirá una exploración completa de la tabla de tblFEStatsPaperHits, lo cual está totalmente bien.
Actualmente, se ejecuta una exploración completa para cada fila en tblFEStatsBrowsers, lo que significa que tengo 553 exploraciones de tabla completa de tblFEStatsPaperHits.
Reescribir solo a WHERE EXISTS no cambia el plan:
DELETE FROM tblFEStatsBrowsers WHERE NOT EXISTS (
SELECT * FROM tblFEStatsPaperHits WITH (NOLOCK) WHERE BrowserID = tblFEStatsBrowsers.BrowserID
)
Sin embargo, como lo sugirió Adam Machanic, agregar una opción HASH JOIN resulta en un plan de ejecución óptimo (solo un escaneo de tblFEStatsPaperHits):
DELETE FROM tblFEStatsBrowsers WHERE NOT EXISTS (
SELECT * FROM tblFEStatsPaperHits WITH (NOLOCK) WHERE BrowserID = tblFEStatsBrowsers.BrowserID
) OPTION (HASH JOIN)
Ahora, esto no es tanto una cuestión de cómo solucionar esto: puedo usar la OPCIÓN (HASH JOIN) o crear una tabla temporal manualmente. Me pregunto por qué el optimizador de consultas usaría el plan que usa actualmente.
Dado que el QO no tiene ninguna estadística en la columna BrowserID, supongo que está asumiendo lo peor: 50 millones de valores distintos, lo que requiere una mesa de trabajo bastante grande en memoria / tempdb. Como tal, la forma más segura es realizar exploraciones para cada fila en tblFEStatsBrowsers. No existe una relación de clave externa entre las columnas BrowserID en las dos tablas, por lo que el QO no puede deducir ninguna información de tblFEStatsBrowsers.
¿Es esta, tan simple como parece, la razón?
Actualización 1
Para dar un par de estadísticas: OPCIÓN (HASH JOIN):
208.711 lecturas lógicas (12 escaneos)
OPCIÓN (LOOP JOIN, HASH GROUP):
11.008.698 lecturas lógicas (~ escaneo por ID de navegador (339))
Sin opciones:
11.008.775 lecturas lógicas (~ escaneo por ID de navegador (339))
Actualización 2
Excelentes respuestas, todos ustedes, ¡gracias! Difícil elegir solo uno. Aunque Martin fue el primero y Remus proporciona una solución excelente, tengo que dárselo al Kiwi para que se vuelva mental en los detalles :)
fuente
Respuestas:
Para decirlo de otra manera, la pregunta es por qué el siguiente plan parece más barato para el optimizador, en comparación con las alternativas (de las cuales hay muchas ).
El lado interno de la unión está ejecutando esencialmente una consulta del siguiente formulario para cada valor correlacionado de
BrowserID
:Tenga en cuenta que el número estimado de filas es 185,220 (no 289,013 ) ya que la comparación de igualdad excluye implícitamente
NULL
(a menos queANSI_NULLS
seaOFF
). El costo estimado del plan anterior es de 206.8 unidades.Ahora agreguemos una
TOP (1)
cláusula:El costo estimado es ahora de 0.00452 unidades. La adición del operador físico superior establece un objetivo de fila de 1 fila en el operador superior. La pregunta se convierte entonces en cómo derivar un 'objetivo de fila' para el Análisis de índice agrupado; es decir, ¿cuántas filas debe procesar el análisis antes de que una fila coincida con el
BrowserID
predicado?La información estadística disponible muestra 166
BrowserID
valores distintos (1 / [Toda la densidad] = 1 / 0.006024096 = 166). El cálculo de costos supone que los valores distintos se distribuyen de manera uniforme sobre las filas físicas, por lo que el objetivo de la fila en el Análisis de índice agrupado se establece en 166.302 (lo que representa el cambio en la cardinalidad de la tabla desde que se recopilaron las estadísticas muestreadas).El costo estimado de escanear las 166 filas esperadas no es muy grande (incluso ejecutado 339 veces, una vez por cada cambio de
BrowserID
): la exploración de índice agrupado muestra un costo estimado de 1.3219 unidades, mostrando el efecto de escala del objetivo de la fila. Los costos del operador sin escala para E / S y CPU se muestran como 153.931 y 52.8698 respectivamente:En la práctica, es muy poco probable que las primeras 166 filas escaneadas del índice (en el orden en que se devuelvan) contendrán uno de cada uno de los
BrowserID
valores posibles . Sin embargo, elDELETE
plan tiene un costo total de 1.40921 unidades, y el optimizador lo selecciona por ese motivo. Bart Duncan muestra otro ejemplo de este tipo en una publicación reciente titulada Row Goals Gone Rogue .También es interesante observar que el operador Top en el plan de ejecución no está asociado con Anti Semi Join (en particular, el Martin de 'cortocircuito' menciona). Podemos comenzar a ver de dónde viene la parte superior deshabilitando primero una regla de exploración llamada GbAggToConstScanOrTop :
Ese plan tiene un costo estimado de 364.912 y muestra que la parte superior reemplazó un grupo por agregado (agrupación por la columna correlacionada
BrowserID
). El agregado no se debe a la redundanciaDISTINCT
en el texto de la consulta: es una optimización que puede ser introducida por dos reglas de exploración, LASJNtoLASJNonDist y LASJOnLclDist . Deshabilitar esos dos también produce este plan:Ese plan tiene un costo estimado de 40729.3 unidades.
Sin la transformación de Agrupar por arriba, el optimizador 'naturalmente' elige un plan de unión hash con
BrowserID
agregación antes de la anti semi unión:Y sin la restricción MAXDOP 1, un plan paralelo:
Otra forma de "arreglar" la consulta original sería crear el índice
BrowserID
que falta en el que informa el plan de ejecución. Los bucles anidados funcionan mejor cuando se indexa el lado interno. Estimar la cardinalidad para semiuniones es un desafío en el mejor de los casos. No tener una indexación adecuada (¡la tabla grande ni siquiera tiene una clave única!) No ayudará en absoluto.Escribí más sobre esto en Row Goals, Part 4: The Anti Join Anti Pattern .
fuente
Cuando ejecuto su script para crear una base de datos solo de estadísticas y la consulta en la pregunta obtengo el siguiente plan.
Las cardinalidades de tabla que se muestran en el plan son
tblFEStatsPaperHits
:48063400
tblFEStatsBrowsers
:339
Por lo tanto, estima que necesitará realizar el escaneo
tblFEStatsPaperHits
339 veces. Cada exploración tiene el predicado correlacionadotblFEStatsBrowsers.BrowserID=tblFEStatsPaperHits.BrowserID AND tblFEStatsPaperHits.BrowserID IS NOT NULL
que se introduce en el operador de exploración.Sin embargo, el plan no significa que habrá 339 escaneos completos. Como se encuentra bajo un operador anti semiunión tan pronto como se encuentra la primera fila coincidente en cada exploración, puede provocar un cortocircuito en el resto. El costo estimado del subárbol para este nodo es
1.32603
y todo el plan tiene un costo1.41337
.Para el Hash Join, da el siguiente plan
El plan general tiene un costo de
418.415
(alrededor de 300 veces más caro que el plan de bucles anidados) con el escaneo de índice agrupado completo solo en untblFEStatsPaperHits
costo206.8
solo. Compárese esto con la1.32603
estimación de 339 exploraciones parciales dadas anteriormente (coste estimado de exploración parcial Moderado =0.003911592
).Esto indicaría que le está costando a cada escaneo parcial 53,000 veces menos costoso que un escaneo completo. Si los costos se escalaran linealmente con el recuento de filas, eso significaría que se supone que, en promedio, solo necesitaría procesar 900 filas en cada iteración antes de encontrar una fila coincidente y hacer un cortocircuito.
Sin embargo, no creo que los costos se escalen de esa manera lineal. Creo que también incorporan algún elemento de costo fijo de inicio. Intentando varios valores de
TOP
en la siguiente consulta147
da el costo estimado de subárbol más cercano a0.003911592
at0.0039113
. De cualquier manera, está claro que basa el costo en el supuesto de que cada escaneo solo tendrá que procesar una pequeña proporción de la tabla, del orden de cientos de filas en lugar de millones.No estoy seguro exactamente en qué matemáticas basa esta suposición y realmente no cuadra con las estimaciones de recuento de filas en el resto del plan (las 236 filas estimadas que salen de la unión de bucles anidados implicarían que había 236 casos en los que no se encontró ninguna fila coincidente y se requirió una exploración completa Supongo que este es solo un caso en el que los supuestos de modelado realizados disminuyen un poco y dejan el plan de bucles anidados significativamente bajo costo.
fuente
En mi libro, incluso un escaneo de 50 millones de filas es inaceptable ... Mi truco habitual es materializar los distintos valores y delegar el motor para mantenerlo actualizado:
Esto le da un índice materializado de una fila por BrowserID, eliminando la necesidad de escanear 50 millones de filas. El motor lo mantendrá por usted y el QO lo usará 'tal cual' en la declaración que publicó (sin ninguna sugerencia o reescritura de consulta).
La desventaja es, por supuesto, la contención. Cualquier operación de inserción o eliminación en
tblFEStatsPaperHits
(y supongo que es una tabla de registro con inserciones pesadas) tendrá que serializar el acceso a un ID de navegador determinado. Hay maneras de hacerlo viable (actualizaciones retrasadas, 2 registros por etapas, etc.) si está dispuesto a comprarlo.fuente