A consulta pausa após retornar um número fixo de linhas

8

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 42ou 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 44ou 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 NOLOCKtodo 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 NULLfiltro descarta a maioria dos registros retornados por BaseView; sem uma TOPclá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 45consulta, 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 JOINmapa por uma NOT EXISTSclá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 NULLconstruçã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 EXISTSclá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 EXISTSser 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.

Jon de todos os comércios
fonte
Comentários não são para discussão prolongada; esta conversa foi movida para o bate-papo .
Paul White 9

Respostas:

4

Algumas coisas para tentar:

  1. Verifique seus índices

    • Todos os JOINcampos-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)

  2. Atualizar estatísticas

    • Pode haver problemas com estatísticas desatualizadas. Se você puder balançar, eu faria um FULLSCAN. Se houver um grande número de linhas, é possível que os dados tenham mudado significativamente sem disparar um recálculo automático.
  3. Limpe a consulta

    • Crie Map JOINum NOT EXISTS- Você não precisa de nenhum dado dessa tabela, pois deseja apenas registros não correspondentes

    • Retire 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.

JNK
fonte
Ponto interessante re: o índice filtrado. A consulta não a usa automaticamente, mas testarei forçá-la com uma dica. Atualizei as estatísticas e posso testar esta e suas outras recomendações ainda hoje; Preciso deixar um acúmulo acumulado após o EOWD para que eu possa testar um conjunto decente de dados.
Jon de todos os negócios
Eu tentei combinações diferentes desses ajustes, e a chave parece ser a anti-junção com o Map. Como LEFT JOIN...WHERE Id IS NULL, eu recebo esta pausa; como uma NOT EXISTScláusula, o tempo de execução é segundos. Estou surpreso, mas não posso discutir com resultados!
Jon de Todos os Negócios
2

Melhoria 1 Remova a SubQuery for Orders e converta-a em junção

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

Melhoria 2 - Mantenha os registros filtrados dos TransactionalCustomers em uma tabela Temporária 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

Ponto 3 - Suponho que você tenha índices em CustomerID, EmailAddress, OrderDate

Pankaj Garg
fonte
11
Re: "Melhoria" 1 - EXISTSé normalmente mais rápido do que um JOINnesta circunstância, e elimina potenciais enganadores. Eu não acho que seria uma melhoria.
JNK
11
o problema é duplo, no entanto - ele potencialmente mudará os resultados e, a menos que ambas as tabelas tenham um índice clusterizado exclusivo nos campos usados ​​na junção, será menos eficiente que um EXISTS. Subcláusulas nem sempre são ruins.
JNK
@PankajGarg: Obrigado pelas sugestões, infelizmente, geralmente existem vários pedidos por cliente, por isso 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.
Jon de todos os negócios