Ich habe eine Ansicht, die schnell (einige Sekunden) für bis zu 41 Datensätze (z. B. TOP 41
) ausgeführt wird, für 44 oder mehr Datensätze jedoch einige Minuten dauert, mit Zwischenergebnissen, wenn sie mit TOP 42
oder ausgeführt wird TOP 43
. Insbesondere werden die ersten 39 Datensätze in wenigen Sekunden zurückgegeben und dann fast drei Minuten lang angehalten, bevor die verbleibenden Datensätze zurückgegeben werden. Dieses Muster ist das gleiche , wenn die Abfrage TOP 44
oder TOP 100
.
Diese Ansicht wurde ursprünglich von einer Basisansicht abgeleitet und fügt der Basis nur einen Filter hinzu, den letzten im folgenden Code. Es scheint keinen Unterschied zu geben, ob ich die untergeordnete Ansicht von der Basis aus verkette oder die untergeordnete Ansicht mit dem Code von der Basis inline schreibe. Die Basisansicht gibt in wenigen Sekunden 100 Datensätze zurück. Ich würde gerne glauben, dass ich die untergeordnete Ansicht so schnell wie die Basis ausführen kann, nicht 50-mal langsamer. Hat jemand diese Art von Verhalten gesehen? Irgendwelche Vermutungen bezüglich Ursache oder Lösung?
Dieses Verhalten war in den letzten Stunden konsistent, da ich die beteiligten Abfragen getestet habe, obwohl die Anzahl der zurückgegebenen Zeilen, bevor sich die Dinge verlangsamen, leicht gestiegen und gesunken ist. Das ist nicht neu; Ich schaue es mir jetzt an, weil die Gesamtlaufzeit akzeptabel war (<2 Minuten), aber ich habe diese Pause zumindest seit Monaten in verwandten Protokolldateien gesehen.
Blockierung
Ich habe die Abfrage noch nie blockiert gesehen, und das Problem besteht auch dann, wenn keine andere Aktivität in der Datenbank vorhanden ist (wie von sp_WhoIsActive bestätigt). Die Basisansicht enthält NOLOCK
durchgehend, was das wert ist.
Abfragen
Hier ist eine abgespeckte Version der untergeordneten Ansicht, wobei die Basisansicht der Einfachheit halber eingefasst ist. Es zeigt immer noch den Laufzeitsprung bei etwa 40 Datensätzen.
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
Dieser Id IS NULL
Filter verwirft die meisten von zurückgegebenen Datensätze BaseView
. Ohne TOP
Klausel geben sie 1.100 Datensätze bzw. 267 KB zurück.
Statistiken
Beim Laufen 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.
Beim Laufen 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.
Ich bin überrascht zu sehen, dass die Anzahl der Lesevorgänge für diesen bescheidenen Unterschied in der tatsächlichen Ausgabe ~ 3x springt.
Beim Vergleich der Ausführungspläne sind sie bis auf die Anzahl der zurückgegebenen Zeilen identisch. Wie bei den obigen Statistiken sind die tatsächlichen Zeilenzahlen für die ersten Schritte in der TOP 45
Abfrage dramatisch höher und nicht nur 12,5% höher.
Im Umriss wird ein Deckungsindex aus Bestellungen gescannt, um entsprechende Datensätze von WarehouseCustomers zu suchen. Loop-Joining mit TransactionalCustomers (Remote-Abfrage, genauer Plan unbekannt); und Zusammenführen mit einem Tabellenscan von AccountsMap. Die Remote-Abfrage beträgt 94% der geschätzten Kosten.
Sonstige Hinweise
Früher, als ich den erweiterten Inhalt der Ansicht als eigenständige Abfrage ausführte, lief sie ziemlich schnell: 13 Sekunden für 100 Datensätze. Ich teste jetzt eine abgespeckte Version der Abfrage ohne Unterabfragen, und diese viel einfachere Abfrage benötigt drei Minuten, um mehr als 40 Zeilen zurückzugeben, selbst wenn sie als eigenständige Abfrage ausgeführt wird.
Die untergeordnete Ansicht enthält eine beträchtliche Anzahl von Lesevorgängen (~ 1 MB pro sp_WhoIsActive), aber auf diesem Computer (acht Kerne, 32 GB RAM, 95% dedizierte SQL-Box) ist dies normalerweise kein Problem.
Ich habe beide Ansichten mehrmals ohne Änderungen gelöscht und neu erstellt.
Die Daten enthalten keine TEXT- oder BLOB-Felder. Ein Feld beinhaltet eine UDF; Das Entfernen verhindert nicht die Pause.
Die Zeiten sind ähnlich, unabhängig davon, ob auf dem Server selbst oder auf meiner 1.400 Meilen entfernten Workstation abgefragt wird. Die Verzögerung scheint also eher der Abfrage selbst zuzuschreiben, als die Ergebnisse an den Client zu senden.
Anmerkungen zu: der Lösung
Das Update war einfach: Ersetzen der LEFT JOIN
to Map durch eine NOT EXISTS
Klausel. Dies führt nur zu einem winzigen Unterschied im Abfrageplan, der nach dem Beitritt zur Map-Tabelle statt zuvor zur TransactionCustomers-Tabelle (einer Remote-Abfrage) hinzugefügt wird. Dies kann bedeuten, dass nur die erforderlichen Datensätze vom Remote-Server angefordert werden, wodurch das übertragene Volumen um das 100-fache verringert würde.
Normalerweise bin ich der erste, der anfeuert NOT EXISTS
; Es ist oft schneller als ein LEFT JOIN...WHERE ID IS NULL
Konstrukt und etwas kompakter. In diesem Fall ist dies umständlich, da die Problemabfrage auf einer vorhandenen Ansicht basiert und das für den Anti-Join erforderliche Feld von der Basisansicht angezeigt wird. Zunächst wird es von einer Ganzzahl in Text umgewandelt. Für eine anständige Leistung muss ich also das zweischichtige Muster ablegen und stattdessen zwei nahezu identische Ansichten haben, wobei die zweite die NOT EXISTS
Klausel enthält.
Vielen Dank für Ihre Hilfe bei der Behebung dieses Problems! Es mag für meine Umstände zu spezifisch sein, um jemand anderem zu helfen, aber hoffentlich nicht. Wenn nichts anderes, ist es ein Beispiel dafür, NOT EXISTS
dass man mehr als nur geringfügig schneller ist als LEFT JOIN...WHERE ID IS NULL
. Die eigentliche Lektion besteht jedoch wahrscheinlich darin, sicherzustellen, dass Remote-Abfragen so effizient wie möglich verknüpft werden. Der Abfrageplan gibt an, dass er 2% der Kosten ausmacht, schätzt jedoch nicht immer genau.
quelle
Antworten:
Einige Dinge zu versuchen:
Überprüfen Sie Ihre Indizes
Sind alle
JOIN
Schlüsselfelder indiziert? Wenn Sie diese Ansicht häufig verwenden, würde ich sogar einen gefilterten Index für die Kriterien in der Ansicht hinzufügen. Zum Beispiel...CREATE INDEX ix_CustomerId ON WarehouseCustomers(CustomerId, EmailAddress) WHERE DateMadeObsolete IS NULL AND AccessKey IN ('C', 'R') AND CustomerID NOT IN (243566)
Statistiken aktualisieren
FULLSCAN
. Wenn eine große Anzahl von Zeilen vorhanden ist, haben sich die Daten möglicherweise erheblich geändert, ohne dass eine automatische Neuberechnung ausgelöst wird.Bereinigen Sie die Abfrage
Machen Sie das
Map
JOIN
aNOT EXISTS
- Sie benötigen keine Daten aus dieser Tabelle, da Sie nur nicht übereinstimmende Datensätze möchtenEntfernen Sie die
ORDER BY
. Ich weiß, dass die Kommentare sagen, dass es keine Rolle spielt, aber ich finde das sehr schwer zu glauben. Für Ihre kleineren Ergebnismengen ist dies möglicherweise nicht von Bedeutung, da die Datenseiten bereits zwischengespeichert sind.quelle
LEFT JOIN...WHERE Id IS NULL
bekomme ich diese Pause; AlsNOT EXISTS
Klausel beträgt die Laufzeit Sekunden. Ich bin überrascht, aber ich kann nicht mit Ergebnissen streiten!Verbesserung 1 Entfernen Sie die SubQuery for Orders und konvertieren Sie sie in Join
Verbesserung 2 - Halten Sie die gefilterten TransactionalCustomers-Datensätze in einer lokalen temporären Tabelle
Letzte Abfrage
Punkt 3 - Ich gehe davon aus, dass Sie Indizes für CustomerID, EmailAddress, OrderDate haben
quelle
EXISTS
ist unterJOIN
diesen Umständen normalerweise schneller als a und eliminiert potenzielle Betrüger. Ich denke nicht, dass es überhaupt eine Verbesserung wäre.EXISTS
ist also obligatorisch. In einer Ansicht kann ich die wiederverwendeten Kundendaten auch nicht zwischenspeichern, obwohl ich mit der Idee eines Dummy-TVF ohne Parameter gespielt habe.