La query viene messa in pausa dopo aver restituito un numero fisso di righe


8

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 42o 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 44o 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 NOLOCKtutto, 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 NULLfiltro scarta la maggior parte dei record restituiti da BaseView; senza una TOPclausola, 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 45query, 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 JOINmappa con una NOT EXISTSclausola. 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 NULLcostrutto 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 EXISTSclausola.

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 EXISTSessere 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.


I commenti non sono per una discussione estesa; questa conversazione è stata spostata in chat .
Paul White 9

Risposte:


4

Alcune cose da provare:

  1. Controlla i tuoi indici

    • Tutti i JOINcampi chiave sono indicizzati? Se usi molto questa vista, arriverei al punto di aggiungere un indice filtrato per i criteri nella vista. Per esempio...

    • CREATE INDEX ix_CustomerId ON WarehouseCustomers(CustomerId, EmailAddress) WHERE DateMadeObsolete IS NULL AND AccessKey IN ('C', 'R') AND CustomerID NOT IN (243566)

  2. Aggiorna statistiche

    • Potrebbero esserci problemi con statistiche non aggiornate. Se riesci a farlo oscillare, farei un FULLSCAN. Se esiste un numero elevato di righe, è possibile che i dati siano cambiati in modo significativo senza attivare un ricalcolo automatico.
  3. Pulisci la query

    • Crea la Map JOINa NOT EXISTS- Non hai bisogno di dati da quella tabella, poiché vuoi solo record non corrispondenti

    • Rimuovi il ORDER BY. So che i commenti dicono che non importa, ma trovo molto difficile da credere. Potrebbe non essere importante per i set di risultati più piccoli poiché le pagine di dati sono già memorizzate nella cache.


Punto interessante riguardo: l'indice filtrato. La query non la utilizza automaticamente, ma proverò a forzarla con un suggerimento. Ho aggiornato le statistiche e posso testare questo e altri consigli più tardi oggi; Ho bisogno di creare un backlog dopo EOWD in modo da poter testare un set di dati decente.
Jon of All Trades,

Ho provato diverse combinazioni di queste modifiche e la chiave sembra essere l'anti-join con Map. Come LEFT JOIN...WHERE Id IS NULL, ottengo questa pausa; come NOT EXISTSclausola, il tempo di esecuzione è di secondi. Sono sorpreso, ma non posso discutere con i risultati!
Jon of All Trades,

2

Miglioramento 1 Rimuovere SubQuery per gli ordini e convertirlo 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

Miglioramento 2: conservare i record filtrati di TransactionalCustomers in una tabella temporanea locale

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)

Query finale

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

Punto 3 - Presumo che tu abbia indici su CustomerID, EmailAddress, OrderDate


1
Ri: "Miglioramento" 1 - EXISTSè normalmente più veloce di un JOINin questa circostanza, ed elimina potenziali duplicati. Non penso che sarebbe un miglioramento.
JNK,

1
il problema è duplice, tuttavia: CAMBIA potenzialmente I RISULTATI e, a meno che entrambe le tabelle non abbiano un indice cluster univoco sui campi utilizzati nel join, sarà meno efficiente di un EXISTS. I sottoclausi non sono sempre male.
JNK,

@PankajGarg: Grazie per i suggerimenti, purtroppo ci sono comunemente più ordini per cliente, quindi EXISTSè obbligatorio. Inoltre, in una vista non posso memorizzare nella cache i dati dei clienti riutilizzati, anche se ho giocato con l'idea di un TVF fittizio senza parametri.
Jon of All Trades,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.