Tengo una vista que se ejecuta rápidamente (unos segundos) para hasta 41 registros (p. Ej. TOP 41
) , Pero toma varios minutos para 44 o más registros, con resultados intermedios si se ejecuta con TOP 42
o TOP 43
. Específicamente, devolverá los primeros 39 registros en unos segundos, luego se detendrá durante casi tres minutos antes de devolver los registros restantes. Este patrón es el mismo cuando se consulta TOP 44
o TOP 100
.
Esta vista originalmente se deriva de una vista base, agregando a la base solo un filtro, el último en el código a continuación. Parece que no hay diferencia si encadeno la vista secundaria desde la base o si escribo la vista secundaria con el código desde la base en línea. La vista base devuelve 100 registros en solo unos segundos. Me gustaría pensar que puedo hacer que la vista infantil se ejecute tan rápido como la base, no 50 veces más lento. ¿Alguien ha visto este tipo de comportamiento? ¿Alguna suposición en cuanto a causa o resolución?
Este comportamiento ha sido constante durante las últimas horas, ya que he probado las consultas involucradas, aunque el número de filas devueltas antes de que las cosas comiencen a disminuir se ha incrementado y disminuido ligeramente. Esto no es nuevo; Lo estoy viendo ahora porque el tiempo de ejecución total había sido aceptable (<2 minutos), pero he visto esta pausa en los archivos de registro relacionados durante meses, al menos.
Bloqueo
Nunca he visto la consulta bloqueada, y el problema existe incluso cuando no hay otra actividad en la base de datos (según lo validado por sp_WhoIsActive). La vista base incluye NOLOCK
todo lo que vale.
Consultas
Aquí hay una versión reducida de la vista secundaria, con la vista base alineada para simplificar. Todavía exhibe el salto en el tiempo de ejecución en aproximadamente 40 registros.
SELECT TOP 100 PERCENT
Map.SalesforceAccountID AS Id,
CAST(C.CustomerID AS NVARCHAR(255)) AS Name,
CASE WHEN C.StreetAddress = 'Unknown' THEN '' ELSE C.StreetAddress END AS BillingStreet,
CASE WHEN C.City = 'Unknown' THEN '' ELSE SUBSTRING(C.City, 1, 40) END AS BillingCity,
SUBSTRING(C.Region, 1, 20) AS BillingState,
CASE WHEN C.PostalCode = 'Unknown' THEN '' ELSE SUBSTRING(C.PostalCode, 1, 20) END AS BillingPostalCode,
CASE WHEN C.Country = 'Unknown' THEN '' ELSE SUBSTRING(C.Country, 1, 40) END AS BillingCountry,
CASE WHEN C.PhoneNumber = 'Unknown' THEN '' ELSE C.PhoneNumber END AS Phone,
CASE WHEN C.FaxNumber = 'Unknown' THEN '' ELSE C.FaxNumber END AS Fax,
TransC.WebsiteAddress AS Website,
C.AccessKey AS AccessKey__c,
CASE WHEN dbo.ValidateEMail(C.EMailAddress) = 1 THEN C.EMailAddress END, -- Removing this UDF does not speed things
TransC.EmailSubscriber
-- A couple dozen additional TransC fields
FROM
WarehouseCustomers AS C WITH (NOLOCK)
INNER JOIN TransactionalCustomers AS TransC WITH (NOLOCK) ON C.CustomerID = TransC.CustomerID
LEFT JOIN Salesforce.AccountsMap AS Map WITH (NOLOCK) ON C.CustomerID = Map.CustomerID
WHERE
C.DateMadeObsolete IS NULL
AND C.EmailAddress NOT LIKE '%@volusion.%'
AND C.AccessKey IN ('C', 'R')
AND C.CustomerID NOT IN (243566) -- Exclude specific test records
AND EXISTS (SELECT * FROM Orders AS O WHERE C.CustomerID = O.CustomerID AND O.OrderDate >= '2010-06-28') -- Only count customers who've placed a recent order
AND Map.SalesforceAccountID IS NULL -- Only count customers not already uploaded to Salesforce
-- Removing the ORDER BY clause does not speed things up
ORDER BY
C.CustomerID DESC
Ese Id IS NULL
filtro descarta la mayoría de los registros devueltos por BaseView
; sin una TOP
cláusula, devuelven 1.100 registros y 267K, respectivamente.
Estadísticas
Cuando se ejecuta TOP 40
:
SQL Server parse and compile time: CPU time = 234 ms, elapsed time = 247 ms.
SQL Server Execution Times: CPU time = 0 ms, elapsed time = 0 ms.
SQL Server Execution Times: CPU time = 0 ms, elapsed time = 0 ms.
(40 row(s) affected)
Table 'CustomersHistory'. Scan count 2, logical reads 39112, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Orders'. Scan count 1, logical reads 752, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'AccountsMap'. Scan count 1, logical reads 458, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
SQL Server Execution Times: CPU time = 2199 ms, elapsed time = 7644 ms.
Cuando se ejecuta TOP 45
:
(45 row(s) affected)
Table 'CustomersHistory'. Scan count 2, logical reads 98268, physical reads 1, read-ahead reads 3, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Orders'. Scan count 1, logical reads 1788, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'AccountsMap'. Scan count 1, logical reads 2152, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
SQL Server Execution Times: CPU time = 41980 ms, elapsed time = 177231 ms.
Me sorprende ver que el número de lecturas aumenta ~ 3 veces para esta modesta diferencia en la salida real.
Comparando los planes de ejecución, son los mismos que no sean el número de filas devueltas. Al igual que con las estadísticas anteriores, los recuentos de filas reales para los primeros pasos son dramáticamente más altos en la TOP 45
consulta, no solo un 12.5% más.
En resumen, está escaneando un índice de cobertura de pedidos, buscando registros correspondientes de WarehouseCustomers; unir esto a TransactionalCustomers (consulta remota, plan exacto desconocido); y fusionando esto con un escaneo de tabla de AccountsMap. La consulta remota es el 94% del costo estimado.
Notas misceláneas
Anteriormente, cuando ejecuté el contenido expandido de la vista como una consulta independiente, se ejecutó bastante rápido: 13 segundos para 100 registros. Ahora estoy probando una versión reducida de la consulta, sin subconsultas, y esta consulta mucho más simple tarda tres minutos en solicitar que devuelva más de 40 filas, incluso cuando se ejecuta como una consulta independiente.
La vista secundaria incluye un número considerable de lecturas (~ 1 M por sp_WhoIsActive), pero en esta máquina (ocho núcleos, 32 GB de RAM, 95% de caja SQL dedicada) eso normalmente no es un problema.
He descartado y recreado ambas vistas varias veces, sin cambios.
Los datos no incluyen ningún campo TEXTO o BLOB. Un campo involucra un UDF; quitarlo no evita la pausa.
Los tiempos son similares tanto si realizo consultas en el servidor como en mi estación de trabajo a 1.400 millas de distancia, por lo que la demora parece ser inherente a la consulta misma en lugar de enviar los resultados al cliente.
Notas sobre la solución
La solución terminó siendo simple: reemplazar el LEFT JOIN
to Map con una NOT EXISTS
cláusula. Esto causa solo una pequeña diferencia en el plan de consulta, uniéndose a la tabla TransactionCustomers (una consulta remota) después de unirse a la tabla Map en lugar de antes. Esto puede significar que solo solicita los registros necesarios del servidor remoto, lo que reduciría el volumen transmitido ~ 100 veces.
Normalmente soy el primero en animar NOT EXISTS
; A menudo es más rápido que una LEFT JOIN...WHERE ID IS NULL
construcción, y un poco más compacto. En este caso, es incómodo porque la consulta del problema se basa en una vista existente, y si bien el campo necesario para la combinación se expone en la vista base, primero se convierte de entero a texto. Entonces, para un rendimiento decente, tengo que descartar el patrón de dos capas y, en cambio, tener dos vistas casi idénticas, la segunda incluye la NOT EXISTS
cláusula.
¡Gracias a todos por su ayuda para solucionar este problema! Puede ser demasiado específico para mis circunstancias como para ayudar a alguien más, pero espero que no. Por lo menos, es un ejemplo de NOT EXISTS
ser más que marginalmente más rápido que LEFT JOIN...WHERE ID IS NULL
. Pero la verdadera lección es probablemente garantizar que las consultas remotas se unan de la manera más eficiente posible; el plan de consulta afirma que representa el 2% del costo, pero no siempre realiza una estimación precisa.
fuente
Respuestas:
Algunas cosas para probar:
Revisa tus índices
¿Están
JOIN
indexados todos los campos clave? Si usa mucho esta vista, iría tan lejos como para agregar un índice filtrado para los criterios en la vista. Por ejemplo...CREATE INDEX ix_CustomerId ON WarehouseCustomers(CustomerId, EmailAddress) WHERE DateMadeObsolete IS NULL AND AccessKey IN ('C', 'R') AND CustomerID NOT IN (243566)
Actualizar estadísticas
FULLSCAN
. Si hay una gran cantidad de filas, es posible que los datos hayan cambiado significativamente sin activar un recálculo automático.Limpia la consulta
Haga la
Map
JOIN
aNOT EXISTS
: no necesita ningún dato de esa tabla, ya que solo desea registros que no coincidanEliminar el
ORDER BY
. Sé que los comentarios dicen que no importa, pero me resulta muy difícil de creer. Es posible que no haya importado para sus conjuntos de resultados más pequeños, ya que las páginas de datos ya están en caché.fuente
LEFT JOIN...WHERE Id IS NULL
, me sale esta pausa; comoNOT EXISTS
cláusula, el tiempo de ejecución es segundos. ¡Estoy sorprendido, pero no puedo discutir con los resultados!Mejora 1 Elimine SubQuery para pedidos y conviértalo en join
Mejora 2: mantenga los registros filtrados de clientes transaccionales en una tabla temporal local
Consulta final
Punto 3: supongo que tiene índices en CustomerID, EmailAddress, OrderDate
fuente
EXISTS
normalmente es más rápido que unJOIN
en esta circunstancia, y elimina posibles engaños. No creo que sea una mejora en absoluto.EXISTS
es obligatorio. Además, en una vista, no puedo almacenar en caché los datos reutilizados del cliente, aunque he jugado con la idea de un TVF ficticio sin parámetros.