Ho una vista che viene eseguita rapidamente (alcuni secondi) per un massimo di 41 record (ad esempio, TOP 41
) ma richiede diversi minuti per 44 o più record, con risultati intermedi se eseguito con TOP 42
o TOP 43
. In particolare, restituirà i primi 39 record in pochi secondi, quindi si arresterà per quasi tre minuti prima di restituire i record rimanenti. Questo modello è lo stesso quando si esegue una query TOP 44
o TOP 100
.
Questa vista originariamente derivava da una vista di base, aggiungendo alla base un solo filtro, l'ultimo nel codice qui sotto. Non sembra esserci alcuna differenza se incatengo la vista figlio dalla base o se scrivo la vista figlio con il codice dalla base allineato. La vista di base restituisce 100 record in pochi secondi. Mi piacerebbe pensare di riuscire a far funzionare la vista del bambino con la stessa velocità della base, non 50 volte più lentamente. Qualcuno ha visto questo tipo di comportamento? Qualche ipotesi sulla causa o sulla risoluzione?
Questo comportamento è stato coerente nelle ultime ore poiché ho testato le query in questione, anche se il numero di righe restituite prima che le cose inizino a rallentare è aumentato leggermente. Questo non è nuovo; Lo sto guardando ora perché il tempo di esecuzione totale era stato accettabile (<2 minuti), ma ho visto questa pausa nei file di registro correlati per almeno mesi.
Blocco
Non ho mai visto la query bloccata e il problema esiste anche quando non ci sono altre attività nel database (come convalidato da sp_WhoIsActive). La vista di base include NOLOCK
tutto, per quello che vale.
Interrogazioni
Ecco una versione ridotta della vista figlio, con la vista di base allineata per semplicità. Mostra ancora il salto nel tempo di esecuzione a circa 40 record.
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
Quel Id IS NULL
filtro scarta la maggior parte dei record restituiti da BaseView
; senza una TOP
clausola, restituiscono rispettivamente 1.100 record e 267K.
statistica
Durante l'esecuzione 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.
Durante l'esecuzione 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.
Sono sorpreso di vedere il numero di letture saltare ~ 3x per questa modesta differenza nell'output effettivo.
Confrontando i piani di esecuzione, sono gli stessi diversi dal numero di righe restituite. Come per le statistiche sopra, i conteggi delle righe effettive per i primi passi sono notevolmente più alti nella TOP 45
query, non solo del 12,5% in più.
In sintesi, sta eseguendo la scansione di un indice di copertura dagli ordini, cercando i record corrispondenti dai clienti Warehouse; unendo questo loop a TransactionalCustomers (query remota, piano esatto sconosciuto); e fondendo questo con una scansione della tabella di AccountsMap. La query remota rappresenta il 94% del costo stimato.
Note varie
In precedenza, quando eseguivo il contenuto espanso della vista come query autonoma, veniva eseguito piuttosto velocemente: 13 secondi per 100 record. Ora sto testando una versione ridotta della query, senza subquery, e questa query molto più semplice richiede tre minuti per chiedere di restituire più di 40 righe, anche quando eseguita come query autonoma.
La vista figlio include un numero considerevole di letture (~ 1M per sp_WhoIsActive), ma su questa macchina (otto core, 32 GB di RAM, casella SQL dedicata al 95%) non è normalmente un problema.
Ho lasciato cadere e ricreato entrambe le viste più volte, senza modifiche.
I dati non includono campi TEXT o BLOB. Un campo coinvolge un UDF; rimuoverlo non impedisce la pausa.
I tempi sono simili sia per le query sul server stesso, sia sulla mia workstation a 1.400 miglia di distanza, quindi il ritardo sembra essere inerente alla query stessa anziché inviare i risultati al client.
Note Re: la soluzione
La correzione ha finito per essere semplice: sostituire la LEFT JOIN
mappa con una NOT EXISTS
clausola. Ciò causa solo una piccola differenza nel piano di query, unendosi alla tabella TransactionCustomers (una query remota) dopo essersi unito alla tabella Mappa anziché a prima. Ciò può significare che sta richiedendo solo i record necessari dal server remoto, il che ridurrebbe il volume trasmesso ~ 100 volte.
Di solito sono il primo a fare il tifo NOT EXISTS
; è spesso più veloce di un LEFT JOIN...WHERE ID IS NULL
costrutto e leggermente più compatto. In questo caso, è imbarazzante perché la query del problema è costruita su una vista esistente e mentre il campo necessario per l'anti-join è esposto dalla vista di base, viene prima trasmesso da intero a testo. Quindi per prestazioni decenti, devo abbandonare il pattern a due livelli e invece ho due viste quasi identiche, con la seconda che include la NOT EXISTS
clausola.
Grazie a tutti per il vostro aiuto nella risoluzione di questo problema! Potrebbe essere troppo specifico per le mie circostanze per essere di aiuto a chiunque altro, ma speriamo di no. Se non altro, è un esempio di NOT EXISTS
essere più che marginalmente più veloce di LEFT JOIN...WHERE ID IS NULL
. Ma la vera lezione è probabilmente quella di garantire che le query remote vengano unite nel modo più efficiente possibile; il piano di query afferma che rappresenta il 2% del costo, ma non sempre stima accuratamente.