Die Abfrage wird angehalten, nachdem eine feste Anzahl von Zeilen zurückgegeben wurde

8

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 42oder 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 44oder 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 NOLOCKdurchgehend, 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 NULLFilter verwirft die meisten von zurückgegebenen Datensätze BaseView. Ohne TOPKlausel 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 45Abfrage 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 JOINto Map durch eine NOT EXISTSKlausel. 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 NULLKonstrukt 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 EXISTSKlausel 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 EXISTSdass 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.

Jon aller Berufe
quelle
Kommentare sind nicht für eine ausführliche Diskussion gedacht. Dieses Gespräch wurde in den Chat verschoben .
Paul White 9

Antworten:

4

Einige Dinge zu versuchen:

  1. Überprüfen Sie Ihre Indizes

    • Sind alle JOINSchlü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)

  2. Statistiken aktualisieren

    • Es könnte Probleme mit veralteten Statistiken geben. Wenn Sie es schwingen können, würde ich eine tun 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.
  3. Bereinigen Sie die Abfrage

    • Machen Sie das Map JOINa NOT EXISTS- Sie benötigen keine Daten aus dieser Tabelle, da Sie nur nicht übereinstimmende Datensätze möchten

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

JNK
quelle
Interessanter Punkt zu: dem gefilterten Index. Die Abfrage verwendet es nicht automatisch, aber ich werde testen, ob ich es mit einem Hinweis erzwinge. Ich habe die Statistiken aktualisiert und kann diese und Ihre anderen Empfehlungen später heute testen. Ich muss nach EOWD einen Rückstand aufbauen lassen, damit ich einen anständigen Datensatz testen kann.
Jon of All Trades
Ich habe verschiedene Kombinationen dieser Optimierungen ausprobiert, und der Schlüssel scheint das Anti-Join mit Map zu sein. Wie LEFT JOIN...WHERE Id IS NULLbekomme ich diese Pause; Als NOT EXISTSKlausel beträgt die Laufzeit Sekunden. Ich bin überrascht, aber ich kann nicht mit Ergebnissen streiten!
Jon of All Trades
2

Verbesserung 1 Entfernen Sie die SubQuery for Orders und konvertieren Sie sie in 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

Verbesserung 2 - Halten Sie die gefilterten TransactionalCustomers-Datensätze in einer lokalen temporären Tabelle

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)

Letzte Abfrage

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

Punkt 3 - Ich gehe davon aus, dass Sie Indizes für CustomerID, EmailAddress, OrderDate haben

Pankaj Garg
quelle
1
Betreff: "Verbesserung" 1 - EXISTSist unter JOINdiesen Umständen normalerweise schneller als a und eliminiert potenzielle Betrüger. Ich denke nicht, dass es überhaupt eine Verbesserung wäre.
JNK
1
Es gibt jedoch zwei Probleme: Es wird möglicherweise die Ergebnisse ändern. Wenn nicht beide Tabellen einen eindeutigen Clustered-Index für die im Join verwendeten Felder haben, ist es weniger effizient als ein EXISTS. Unterabschnitte sind nicht immer schlecht.
JNK
@PankajGarg: Vielen Dank für die Vorschläge, leider gibt es in der Regel mehrere Bestellungen pro Kunde, EXISTSist 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.
Jon of All Trades