J'ai une vue qui s'exécute rapidement (quelques secondes) jusqu'à 41 enregistrements (par exemple, TOP 41
) mais prend plusieurs minutes pour 44 enregistrements ou plus, avec des résultats intermédiaires si exécutés avec TOP 42
ou TOP 43
. Plus précisément, il renverra les 39 premiers enregistrements en quelques secondes, puis s'arrêtera pendant près de trois minutes avant de rendre les enregistrements restants. Ce modèle est le même lors de la requête TOP 44
ou TOP 100
.
Cette vue dérivait à l'origine d'une vue de base, ajoutant à la base un seul filtre, le dernier dans le code ci-dessous. Il ne semble pas y avoir de différence si j'enchaîne la vue enfant depuis la base ou si j'écris la vue enfant avec le code de la base en ligne. La vue de base renvoie 100 enregistrements en quelques secondes. J'aimerais penser que je peux faire fonctionner la vue enfant aussi rapidement que la base, pas 50 fois plus lentement. Quelqu'un a-t-il vu ce genre de comportement? Des suppositions sur la cause ou la résolution?
Ce comportement a été cohérent au cours des dernières heures car j'ai testé les requêtes impliquées, bien que le nombre de lignes renvoyées avant que les choses ne commencent à ralentir ait légèrement augmenté. Ce n'est pas nouveau; Je le regarde maintenant parce que le temps d'exécution total était acceptable (<2 minutes), mais j'ai vu cette pause dans les fichiers journaux associés pendant au moins des mois.
Blocage
Je n'ai jamais vu la requête bloquée, et le problème existe même lorsqu'il n'y a aucune autre activité sur la base de données (telle que validée par sp_WhoIsActive). La vue de base comprend NOLOCK
tout au long de ce que cela vaut.
Requêtes
Voici une version réduite de la vue enfant, avec la vue de base intégrée pour plus de simplicité. Il présente toujours le saut dans le temps d'exécution à environ 40 enregistrements.
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
Ce Id IS NULL
filtre supprime la plupart des enregistrements renvoyés par BaseView
; sans TOP
clause, ils renvoient respectivement 1 100 enregistrements et 267 Ko.
Statistiques
Lors de l'exécution 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.
Lors de l'exécution 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.
Je suis surpris de voir le nombre de lectures bondir d'environ 3 fois pour cette modeste différence de sortie réelle.
En comparant les plans d'exécution, ils sont identiques, à l'exception du nombre de lignes renvoyées. Comme avec les statistiques ci-dessus, le nombre réel de lignes pour les premières étapes est considérablement plus élevé dans la TOP 45
requête, pas seulement 12,5% plus élevé.
En résumé, il analyse un index de couverture à partir des commandes, en recherchant les enregistrements correspondants auprès de WarehouseCustomers; joindre cette boucle à TransactionalCustomers (requête distante, plan exact inconnu); et la fusionner avec une analyse de table de AccountsMap. La requête à distance représente 94% du coût estimé.
Notes diverses
Plus tôt, lorsque j'ai exécuté le contenu étendu de la vue en tant que requête autonome, cela s'est déroulé assez rapidement: 13 secondes pour 100 enregistrements. Je teste maintenant une version réduite de la requête, sans sous-requêtes, et cette requête beaucoup plus simple prend trois minutes pour demander de renvoyer plus de 40 lignes, même lorsqu'elle est exécutée en tant que requête autonome.
La vue enfant comprend un nombre important de lectures (~ 1 M par sp_WhoIsActive), mais sur cette machine (huit cœurs, 32 Go de RAM, 95% de boîte SQL dédiée), ce n'est normalement pas un problème.
J'ai supprimé et recréé les deux vues plusieurs fois, sans aucun changement.
Les données ne comprennent aucun champ TEXT ou BLOB. Un domaine implique un UDF; le retirer n'empêche pas la pause.
Les temps sont similaires, que ce soit sur le serveur lui-même ou sur mon poste de travail à 1 400 miles de distance, donc le délai semble être inhérent à la requête elle-même plutôt que d'envoyer les résultats au client.
Notes Re: la solution
Le correctif a fini par être simple: remplacer le LEFT JOIN
to Map par une NOT EXISTS
clause. Cela ne provoque qu'une seule petite différence dans le plan de requête, la jointure à la table TransactionCustomers (une requête distante) après la jointure à la table Map au lieu d'avant. Cela peut signifier qu'il ne demande que les enregistrements nécessaires au serveur distant, ce qui réduirait le volume transmis ~ 100 fois.
D'ordinaire, je suis le premier à applaudir NOT EXISTS
; il est souvent plus rapide qu'une LEFT JOIN...WHERE ID IS NULL
construction et légèrement plus compact. Dans ce cas, c'est gênant car la requête problématique est construite sur une vue existante, et bien que le champ nécessaire à l'anti-jointure soit exposé par la vue de base, il est d'abord converti de l'entier en texte. Donc, pour des performances décentes, je dois supprimer le modèle à deux couches et avoir à la place deux vues presque identiques, la seconde comprenant la NOT EXISTS
clause.
Merci à tous pour votre aide dans la résolution de ce problème! Cela peut être trop spécifique à ma situation pour aider quelqu'un d'autre, mais j'espère que non. Si rien d'autre, c'est un exemple d' NOT EXISTS
être plus que légèrement plus rapide que LEFT JOIN...WHERE ID IS NULL
. Mais la vraie leçon est probablement de s'assurer que les requêtes distantes sont jointes aussi efficacement que possible; le plan de requête prétend qu'il représente 2% du coût, mais il n'est pas toujours estimé avec précision.
la source
Réponses:
Quelques choses à essayer:
Vérifiez vos index
Tous les
JOIN
champs clés sont-ils indexés? Si vous utilisez beaucoup cette vue, j'irais jusqu'à ajouter un index filtré pour les critères dans la vue. Par exemple...CREATE INDEX ix_CustomerId ON WarehouseCustomers(CustomerId, EmailAddress) WHERE DateMadeObsolete IS NULL AND AccessKey IN ('C', 'R') AND CustomerID NOT IN (243566)
Mettre à jour les statistiques
FULLSCAN
. S'il y a un grand nombre de lignes, il est possible que les données aient considérablement changé sans déclencher un recalcul automatique.Nettoyer la requête
Faites
Map
JOIN
unNOT EXISTS
- Vous n'avez pas besoin de données de cette table, car vous ne voulez que des enregistrements non correspondantsRetirez le
ORDER BY
. Je sais que les commentaires disent que cela n'a pas d'importance, mais je trouve cela très difficile à croire. Cela peut ne pas avoir d'importance pour vos jeux de résultats plus petits car les pages de données sont déjà mises en cache.la source
LEFT JOIN...WHERE Id IS NULL
, je reçois cette pause; en tant queNOT EXISTS
clause, le temps d'exécution est en secondes. Je suis surpris, mais je ne peux pas contester les résultats!Amélioration 1 Supprimez la sous-requête pour les commandes et convertissez-la en jointure
Amélioration 2 - Conserver les enregistrements filtrés TransactionalCustomers dans une table temporaire locale
Requête finale
Point 3 - Je suppose que vous avez des index sur CustomerID, EmailAddress, OrderDate
la source
EXISTS
est normalement plus rapide queJOIN
dans ce cas, et élimine les dupes potentiels. Je ne pense pas que ce serait une amélioration du tout.EXISTS
c'est obligatoire. De plus, dans une vue, je ne peux pas mettre en cache les données client réutilisées, même si j'ai joué avec l'idée d'un TVF factice sans paramètres.