La consulta se detiene después de devolver un número fijo de filas

8

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 42o 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 44o 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 NOLOCKtodo 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 NULLfiltro descarta la mayoría de los registros devueltos por BaseView; sin una TOPclá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 45consulta, 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 JOINto Map con una NOT EXISTSclá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 NULLconstrucció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 EXISTSclá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 EXISTSser 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.

Jon de todos los oficios
fuente
Los comentarios no son para discusión extendida; Esta conversación se ha movido al chat .
Paul White 9

Respuestas:

4

Algunas cosas para probar:

  1. Revisa tus índices

    • ¿Están JOINindexados 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)

  2. Actualizar estadísticas

    • Podría haber problemas con las estadísticas desactualizadas. Si puedes balancearlo, yo haría un FULLSCAN. Si hay una gran cantidad de filas, es posible que los datos hayan cambiado significativamente sin activar un recálculo automático.
  3. Limpia la consulta

    • Haga la Map JOINa NOT EXISTS: no necesita ningún dato de esa tabla, ya que solo desea registros que no coincidan

    • Eliminar 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é.

JNK
fuente
Punto interesante re: el índice filtrado. La consulta no la usa automáticamente, pero probaré forzándola con una pista. He actualizado las estadísticas y puedo probar esta y sus otras recomendaciones más tarde hoy; Necesito dejar que se acumule una acumulación después de EOWD para poder probar con un conjunto de datos decente.
Jon of All Trades
He intentado diferentes combinaciones de estos ajustes, y la clave parece ser la anti-unión con Map. Como LEFT JOIN...WHERE Id IS NULL, me sale esta pausa; como NOT EXISTScláusula, el tiempo de ejecución es segundos. ¡Estoy sorprendido, pero no puedo discutir con los resultados!
Jon of All Trades
2

Mejora 1 Elimine SubQuery para pedidos y conviértalo en join

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
INNER Join Orders AS O 
                                                        ON C.CustomerID = O.CustomerID

 WHERE
    C.DateMadeObsolete IS NULL
    AND C.EmailAddress NOT LIKE '%@volusion.%'
    AND C.AccessKey IN ('C', 'R')
    AND C.CustomerID NOT IN (243566)
    AND O.OrderDate >= '2010-06-28'
    AND Map.SalesforceAccountID IS NULL

Mejora 2: mantenga los registros filtrados de clientes transaccionales en una tabla temporal local

Select 
    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,
    C.AccessKey AS AccessKey__c
Into #Temp
From  WarehouseCustomers C
Where C.DateMadeObsolete IS NULL
        AND C.EmailAddress NOT LIKE '%@volusion.%'
        AND C.AccessKey IN ('C', 'R')
        AND C.CustomerID NOT IN (243566)

Consulta final

FROM
#Temp 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
INNER Join Orders AS O 
                                                            ON C.CustomerID = O.CustomerID

WHERE
C.DateMadeObsolete IS NULL
AND C.EmailAddress NOT LIKE '%@volusion.%'
AND C.AccessKey IN ('C', 'R')
AND C.CustomerID NOT IN (243566)
AND O.OrderDate >= '2010-06-28'
AND Map.SalesforceAccountID IS NULL

Punto 3: supongo que tiene índices en CustomerID, EmailAddress, OrderDate

Pankaj Garg
fuente
1
Re: "Mejora" 1 - EXISTSnormalmente es más rápido que un JOINen esta circunstancia, y elimina posibles engaños. No creo que sea una mejora en absoluto.
JNK
1
Sin embargo, el problema es doble: potencialmente CAMBIARÁ LOS RESULTADOS, y a menos que ambas tablas tengan un índice agrupado único en los campos utilizados en la unión, será menos eficiente que un EXISTE. Las subcláusulas no siempre son malas.
JNK
@PankajGarg: Gracias por las sugerencias, desafortunadamente, comúnmente hay múltiples pedidos por cliente, por lo que EXISTSes 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.
Jon of All Trades