Il piano è stato compilato su un'istanza di SQL Server 2008 R2 RTM (build 10.50.1600). È necessario installare il Service Pack 3 (build 10.50.6000), seguito dalle patch più recenti per portarlo all'ultima (attuale) build 10.50.6542. Questo è importante per una serie di motivi, tra cui sicurezza, correzioni di bug e nuove funzionalità.
Il parametro Incorporamento dell'ottimizzazione
Rilevante per la presente domanda, SQL Server 2008 R2 RTM non supportava l'ottimizzazione dell'incorporamento dei parametri (PEO) per OPTION (RECOMPILE)
. In questo momento, stai pagando il costo di ricompilazioni senza realizzare uno dei principali vantaggi.
Quando PEO è disponibile, SQL Server può utilizzare i valori letterali archiviati in variabili e parametri locali direttamente nel piano di query. Ciò può portare a drammatiche semplificazioni e aumenti delle prestazioni. Ulteriori informazioni al riguardo nel mio articolo, Parameter Sniffing, Embedding e le opzioni RECOMPILE .
Hash, ordinamento e scambio di sversamenti
Questi vengono visualizzati nei piani di esecuzione solo quando la query è stata compilata su SQL Server 2012 o versione successiva. Nelle versioni precedenti, dovevamo monitorare gli sversamenti mentre la query veniva eseguita utilizzando Profiler o Extended Events. Gli sversamenti provocano sempre l'I / O fisico verso (e da) il tempdb di supporto di archiviazione persistente , che può avere importanti conseguenze sulle prestazioni, specialmente se lo sversamento è grande o il percorso di I / O è sotto pressione.
Nel tuo piano di esecuzione, ci sono due operatori Hash Match (aggregati). La memoria riservata per la tabella hash si basa sulla stima per le righe di output (in altre parole, è proporzionale al numero di gruppi trovati in fase di esecuzione). La memoria concessa viene riparata poco prima dell'inizio dell'esecuzione e non può crescere durante l'esecuzione, indipendentemente dalla quantità di memoria libera dell'istanza. Nel piano fornito, entrambi gli operatori Hash Match (aggregato) producono più righe di quelle previste dall'ottimizzatore e pertanto potrebbero verificarsi versamenti di tempdb in fase di esecuzione.
Nel piano è presente anche un operatore Hash Match (Inner Join). La memoria riservata per la tabella hash si basa su stima per le righe di input lato sonda . L'ingresso della sonda stima 847.399 righe, ma 1.223.636 sono state rilevate in fase di esecuzione. Questo eccesso può anche causare una fuoriuscita di hash.
Aggregato ridondante
L'hash match (aggregato) sul nodo 8 esegue un'operazione di raggruppamento su (Assortment_Id, CustomAttrID)
, ma le righe di input sono uguali alle righe di output:
Ciò suggerisce che la combinazione di colonne è una chiave (quindi il raggruppamento non è semanticamente necessario). Il costo dell'esecuzione dell'aggregato ridondante è aumentato dalla necessità di passare due volte 1,4 milioni di righe attraverso gli scambi di partizionamento hash (gli operatori di parallelismo su entrambi i lati).
Dato che le colonne coinvolte provengono da tabelle diverse, è più difficile del solito comunicare queste informazioni di unicità all'ottimizzatore, in modo da evitare l'operazione di raggruppamento ridondante e scambi inutili.
Distribuzione del thread inefficiente
Come notato nella risposta di Joe Obbish , lo scambio nel nodo 14 utilizza il partizionamento hash per distribuire le righe tra i thread. Sfortunatamente, il piccolo numero di righe e gli scheduler disponibili significa che tutte e tre le righe finiscono su un singolo thread. Il piano apparentemente parallelo corre in serie (con sovraccarico parallelo) fino allo scambio nel nodo 9.
È possibile risolvere questo problema (per ottenere il round robin o il partizionamento broadcast) eliminando l'ordinamento distinto nel nodo 13. Il modo più semplice per farlo è creare una chiave primaria cluster sulla #temp
tabella ed eseguire l'operazione distinta quando si carica la tabella:
CREATE TABLE #Temp
(
id integer NOT NULL PRIMARY KEY CLUSTERED
);
INSERT #Temp
(
id
)
SELECT DISTINCT
CAV.id
FROM @customAttrValIds AS CAV
WHERE
CAV.id IS NOT NULL;
Memorizzazione temporanea delle statistiche delle tabelle
Nonostante l'uso di OPTION (RECOMPILE)
, SQL Server può ancora memorizzare nella cache l'oggetto tabella temporanea e le statistiche associate tra le chiamate di procedura. In genere si tratta di una gradita ottimizzazione delle prestazioni, ma se la tabella temporanea viene popolata con una quantità simile di dati su chiamate di procedura adiacenti, il piano ricompilato potrebbe basarsi su statistiche errate (memorizzate nella cache da un'esecuzione precedente). Questo è dettagliato nei miei articoli, nelle tabelle temporanee nelle procedure memorizzate e nella cache temporanea delle tabelle spiegate .
Per evitare ciò, utilizzare OPTION (RECOMPILE)
insieme a un esplicitoUPDATE STATISTICS #TempTable
dopo la tabella temporanea è popolata, e prima che sia fatto riferimento in una query.
Riscrittura query
Questa parte presuppone le modifiche alla creazione di #Temp
tabella siano già state apportate.
Dati i costi di possibili fuoriuscite di hash e l'aggregato ridondante (e gli scambi circostanti), può pagare per materializzare l'insieme nel nodo 10:
CREATE TABLE #Temp2
(
CustomAttrID integer NOT NULL,
Assortment_Id integer NOT NULL,
);
INSERT #Temp2
(
Assortment_Id,
CustomAttrID
)
SELECT
ACAV.Assortment_Id,
CAV.CustomAttrID
FROM #temp AS T
JOIN dbo.CustomAttributeValues AS CAV
ON CAV.Id = T.id
JOIN dbo.AssortmentCustomAttributeValues AS ACAV
ON T.id = ACAV.CustomAttributeValue_Id;
ALTER TABLE #Temp2
ADD CONSTRAINT PK_#Temp2_Assortment_Id_CustomAttrID
PRIMARY KEY CLUSTERED (Assortment_Id, CustomAttrID);
Il PRIMARY KEY
viene aggiunto in una fase separata per garantire la creazione dell'indice ha informazioni accurate cardinalità, e per evitare le statistiche di tabella temporanei caching problema.
È probabile che questa materializzazione si verifichi in memoria (evitando l' I / O tempdb ) se l'istanza ha memoria sufficiente. Ciò è ancora più probabile dopo l'aggiornamento a SQL Server 2012 (SP1 CU10 / SP2 CU1 o versione successiva), che ha migliorato il comportamento Eager Write .
Questa azione fornisce all'ottimizzatore informazioni precise sulla cardinalità sul set intermedio, consente di creare statistiche e ci consente di dichiarare (Assortment_Id, CustomAttrID)
come chiave.
Il piano per la popolazione di #Temp2
dovrebbe assomigliare a questo (notare la scansione dell'indice cluster di #Temp
, nessun ordinamento distinto, e lo scambio ora utilizza il partizionamento di righe round-robin):
Con tale set disponibile, la query finale diventa:
SELECT
A.Id,
A.AssortmentId
FROM
(
SELECT
T.Assortment_Id
FROM #Temp2 AS T
GROUP BY
T.Assortment_Id
HAVING
COUNT_BIG(DISTINCT T.CustomAttrID) = @dist_ca_id
) AS DT
JOIN dbo.Assortments AS A
ON A.Id = DT.Assortment_Id
WHERE
A.AssortmentType = @asType
OPTION (RECOMPILE);
Potremmo riscrivere manualmente il COUNT_BIG(DISTINCT...
come semplice COUNT_BIG(*)
, ma con le nuove informazioni chiave, l'ottimizzatore lo fa per noi:
Il piano finale può utilizzare un loop / hash / merge join a seconda delle informazioni statistiche sui dati a cui non ho accesso. Un'altra piccola nota: ho assunto che CREATE [UNIQUE?] NONCLUSTERED INDEX IX_ ON dbo.Assortments (AssortmentType, Id, AssortmentId);
esista un indice come .
Comunque, la cosa importante dei piani finali è che le stime dovrebbero essere molto migliori, e la complessa sequenza di operazioni di raggruppamento è stata ridotta a un singolo Stream Aggregate (che non richiede memoria e quindi non può essere trasferito su disco).
È difficile dire che le prestazioni saranno effettivamente migliori in questo caso con la tabella temporanea aggiuntiva, ma le stime e le scelte del piano saranno molto più resistenti alle variazioni nel volume e nella distribuzione dei dati nel tempo. Questo potrebbe essere più prezioso a lungo termine di un piccolo aumento delle prestazioni oggi. In ogni caso, ora hai molte più informazioni su cui basare la tua decisione finale.
#temp
creazione e l'uso siano un problema per le prestazioni, non un guadagno. Stai salvando su una tabella non indicizzata per usarla una sola volta. Prova a rimuoverlo completamente (e possibilmente modificandoloin (select id from #temp)
in unaexists
sottoquery.