Eu tenho uma visão que é executada rapidamente (alguns segundos) por até 41 registros (por exemplo, TOP 41
) , mas leva vários minutos para 44 ou mais registros, com resultados intermediários se executados com TOP 42
ou TOP 43
. Especificamente, ele retornará os primeiros 39 registros em alguns segundos e depois será interrompido por quase três minutos antes de retornar os registros restantes. Esse padrão é o mesmo ao consultar TOP 44
ou TOP 100
.
Essa visualização foi originalmente derivada de uma visualização base, adicionando à base apenas um filtro, o último no código abaixo. Parece não haver diferença se eu encadear a visão filho da base ou se eu escrever a visão filho com o código da base alinhado. A exibição base retorna 100 registros em apenas alguns segundos. Gostaria de pensar que posso fazer com que a visão infantil corra tão rapidamente quanto a base, nem 50 vezes mais devagar. Alguém viu esse tipo de comportamento? Alguma sugestão de causa ou resolução?
Esse comportamento tem sido consistente nas últimas horas, pois eu testei as consultas envolvidas, embora o número de linhas retornadas antes que as coisas comecem a desacelerar suba e desça levemente. Isto não é novo; Estou analisando agora porque o tempo total de execução foi aceitável (<2 minutos), mas vi essa pausa nos arquivos de log relacionados há meses, pelo menos.
Bloqueio
Nunca vi a consulta bloqueada e o problema existe mesmo quando não há outra atividade no banco de dados (conforme validado por sp_WhoIsActive). A visão base inclui NOLOCK
todo o conteúdo, pelo que vale a pena.
Consultas
Aqui está uma versão reduzida da exibição filho, com a exibição base alinhada para simplificar. Ele ainda exibe o salto no tempo de execução em cerca de 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
Esse Id IS NULL
filtro descarta a maioria dos registros retornados por BaseView
; sem uma TOP
cláusula, eles retornam 1.100 registros e 267 mil, respectivamente.
Estatisticas
Ao executar 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.
Ao executar 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.
Estou surpreso ao ver o número de leituras saltar ~ 3x para esta modesta diferença na saída real.
Comparando os planos de execução, eles são iguais, exceto o número de linhas retornadas. Como nas estatísticas acima, a contagem real de linhas para as etapas iniciais é muito maior na TOP 45
consulta, e não apenas 12,5% maior.
Em resumo, está digitalizando um índice de cobertura de Pedidos, buscando registros correspondentes de WarehouseCustomers; ingressar em loop para TransactionalCustomers (consulta remota, plano exato desconhecido); e mesclando isso com uma verificação de tabela do AccountsMap. A consulta remota é 94% do custo estimado.
Notas diversas
Anteriormente, quando eu executei o conteúdo expandido da exibição como uma consulta independente, ela foi executada rapidamente: 13 segundos para 100 registros. Agora estou testando uma versão reduzida da consulta, sem subconsultas, e essa consulta muito mais simples leva três minutos para ser solicitada a devolução de mais de 40 linhas, mesmo quando executada como uma consulta independente.
A exibição filho inclui um número substancial de leituras (~ 1 milhão por sp_WhoIsActive), mas nesta máquina (oito núcleos, 32 GB de RAM, caixa SQL dedicada a 95%) isso normalmente não é um problema.
Larguei e recriei as duas visualizações várias vezes, sem alterações.
Os dados não incluem nenhum campo TEXT ou BLOB. Um campo envolve uma UDF; removê-lo não impede a pausa.
Os tempos são semelhantes, seja consultando no próprio servidor ou na minha estação de trabalho, a 2.400 milhas de distância; portanto, o atraso parece ser inerente à própria consulta, em vez de enviar os resultados ao cliente.
Notas Re: a solução
A correção acabou sendo simples: substituindo o LEFT JOIN
mapa por uma NOT EXISTS
cláusula. Isso causa apenas uma pequena diferença no plano de consulta, ingressando na tabela TransactionCustomers (uma consulta remota) após ingressar na tabela Map em vez de antes. Isso pode significar que ele está solicitando apenas os registros necessários do servidor remoto, o que reduziria o volume transmitido ~ 100 vezes.
Normalmente sou o primeiro a torcer NOT EXISTS
; geralmente é mais rápido que uma LEFT JOIN...WHERE ID IS NULL
construção e um pouco mais compacto. Nesse caso, é estranho porque a consulta do problema é criada em uma visualização existente e, enquanto o campo necessário para a anti-junção é exposto pela visualização base, ele é convertido primeiro de inteiro para texto. Portanto, para obter um desempenho decente, preciso descartar o padrão de duas camadas e, em vez disso, ter duas visualizações quase idênticas, com a segunda incluindo a NOT EXISTS
cláusula.
Obrigado a todos pela ajuda na solução deste problema! Pode ser muito específico para as minhas circunstâncias ser útil para qualquer outra pessoa, mas espero que não. Se nada mais, é um exemplo de NOT EXISTS
ser mais do que marginalmente mais rápido que LEFT JOIN...WHERE ID IS NULL
. Mas a verdadeira lição é provavelmente garantir que as consultas remotas sejam unidas da maneira mais eficiente possível; o plano de consulta afirma que representa 2% do custo, mas nem sempre é estimado com precisão.
fonte
Respostas:
Algumas coisas para tentar:
Verifique seus índices
Todos os
JOIN
campos-chave estão indexados? Se você usa muito essa visualização, eu chegaria ao ponto de adicionar um índice filtrado para os critérios na visualização. Por exemplo...CREATE INDEX ix_CustomerId ON WarehouseCustomers(CustomerId, EmailAddress) WHERE DateMadeObsolete IS NULL AND AccessKey IN ('C', 'R') AND CustomerID NOT IN (243566)
Atualizar estatísticas
FULLSCAN
. Se houver um grande número de linhas, é possível que os dados tenham mudado significativamente sem disparar um recálculo automático.Limpe a consulta
Crie
Map
JOIN
umNOT EXISTS
- Você não precisa de nenhum dado dessa tabela, pois deseja apenas registros não correspondentesRetire o
ORDER BY
. Eu sei que os comentários dizem que isso não importa, mas acho isso muito difícil de acreditar. Pode não ter importado para seus conjuntos de resultados menores, pois as páginas de dados já estão armazenadas em cache.fonte
LEFT JOIN...WHERE Id IS NULL
, eu recebo esta pausa; como umaNOT EXISTS
cláusula, o tempo de execução é segundos. Estou surpreso, mas não posso discutir com resultados!Melhoria 1 Remova a SubQuery for Orders e converta-a em junção
Melhoria 2 - Mantenha os registros filtrados dos TransactionalCustomers em uma tabela Temporária Local
Consulta final
Ponto 3 - Suponho que você tenha índices em CustomerID, EmailAddress, OrderDate
fonte
EXISTS
é normalmente mais rápido do que umJOIN
nesta circunstância, e elimina potenciais enganadores. Eu não acho que seria uma melhoria.EXISTS
é obrigatório. Além disso, em uma exibição, não posso armazenar em cache os dados reutilizados do cliente, embora tenha brincado com a idéia de um TVF fictício sem parâmetros.