Modifica query per migliorare le stime dell'operatore


14

Ho una query che viene eseguita in un periodo di tempo accettabile, ma desidero sfruttare al massimo le prestazioni possibili.

L'operazione che sto cercando di migliorare è la "Ricerca indice" a destra del piano, dal Nodo 17.

inserisci qui la descrizione dell'immagine

Ho aggiunto indici appropriati, ma le stime che ottengo per quell'operazione sono la metà di quello che dovrebbero essere.

Ho cercato di modificare i miei indici, aggiungere una tabella temporanea e riscrivere la query, ma non ho potuto semplificarlo più di così per ottenere le stime giuste.

Qualcuno ha qualche suggerimento su cos'altro posso provare?

Il piano completo e i suoi dettagli sono disponibili qui .

Il piano non anonimo può essere trovato qui.

Aggiornare:

Ho la sensazione che la versione iniziale della domanda abbia suscitato molta confusione, quindi aggiungerò il codice originale con alcune spiegazioni.

create procedure [dbo].[someProcedure] @asType int, @customAttrValIds idlist readonly
as
begin
    set nocount on;

    declare @dist_ca_id int;

    select *
    into #temp
    from @customAttrValIds
        where id is not null;

    select @dist_ca_id = count(distinct CustomAttrID) 
    from CustomAttributeValues c
        inner join #temp a on c.Id = a.id;

    select a.Id
        , a.AssortmentId 
    from Assortments a
        inner join AssortmentCustomAttributeValues acav
            on a.Id = acav.Assortment_Id
        inner join CustomAttributeValues cav 
            on cav.Id = acav.CustomAttributeValue_Id
    where a.AssortmentType = @asType
        and acav.CustomAttributeValue_Id in (select id from #temp)
    group by a.AssortmentId
        , a.Id
    having count(distinct cav.CustomAttrID) = @dist_ca_id
    option(recompile);

end

risposte:

  1. Perché la strana denominazione iniziale nel collegamento pasteThePlan?

    Risposta : Perché ho usato il piano di anonimizzazione da SQL Sentry Plan Explorer.

  2. Perché OPTION RECOMPILE?

    Risposta : Perché posso permettermi ricompilazioni per evitare lo sniffing dei parametri (i dati sono / potrebbero essere distorti). Ho testato e sono soddisfatto del piano generato dall'ottimizzatore durante l'utilizzo OPTION RECOMPILE.

  3. WITH SCHEMABINDING?

    Risposta : Vorrei davvero evitarlo e lo userei solo quando ho una vista indicizzata. Comunque, questa è una funzione di sistema ( COUNT()) quindi non serve a niente SCHEMABINDINGqui.

Risposte a più domande possibili:

  1. Perché uso INSERT INTO #temp FROM @customAttrributeValues?

    Risposta : poiché ho notato e ora so che quando si utilizzano le variabili inserite in una query, tutte le stime risultanti dal funzionamento con una variabile sono sempre 1. E ho provato a mettere i dati in una tabella temporanea e la stima è quindi uguale alle righe effettive .

  2. Perché l'ho usato and acav.CustomAttributeValue_Id in (select id from #temp)?

    Risposta : avrei potuto sostituirlo con un JOIN su #temp, ma gli sviluppatori erano molto confusi e hanno offerto l' INopzione. Non credo davvero che ci sarebbe differenza anche sostituendo e in entrambi i casi, non c'è alcun problema con questo.


Immagino che la #tempcreazione 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 modificandolo in (select id from #temp)in una existssottoquery.
ypercubeᵀᴹ

@ ypercubeᵀᴹ Vero, solo poche pagine in meno lette con l'utilizzo della variabile anziché di una tabella temporanea.
Radu Gheorghiu,

A proposito, una variabile di tabella fornirà la stima del conteggio delle righe corretta se utilizzata con Opzione (Ricompila) - ma non ha ancora statistiche granulari, cardinalità ecc.
TH

@TH Bene, ho guardato nel piano di esecuzione reale le stime, quando si utilizzava select id from @customAttrValIdsinvece di select id from #tempe il numero stimato di righe era 1per la variabile e 3per #temp (che corrispondeva al numero effettivo di righe). È per questo che ho sostituito @con #. E io DO ricordare un discorso (da Brent O o Aaron Bertrand) dove hanno detto che quando si utilizza una variabile TBL le stime per questo sarà sempre 1. E come un miglioramento per ottenere stime migliori avrebbero usato una tabella temporanea.
Radu Gheorghiu,

@RaduGheorghiu Sì, ma nel mondo di quei ragazzi, l'opzione (ricompilare) è raramente un'opzione, e preferiscono anche le tabelle temporanee per altri validi motivi. Forse il preventivo mostra semplicemente sempre erroneamente come 1, in quanto modifica il piano come mostrato qui: theboreddba.com/Categories/FunWithFlags/…
TH

Risposte:


12

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:

Nodo 8 Hash Match (aggregato)

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 #temptabella 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 KEYviene 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 #Temp2dovrebbe 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):

# Popolazione Temp2

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:

Piano finale

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.


9

Le stime di cardinalità sulla tua query sono in realtà molto buone. È raro che il numero di righe stimate corrisponda esattamente al numero di righe effettive, soprattutto quando si hanno così tanti join. Unire le stime sulla cardinalità è complicato per l'ottimizzatore per avere ragione. Una cosa importante da notare è che il numero di righe stimate per la parte interna del loop nidificato è per esecuzione di quel loop. Quindi, quando SQL Server dice che verranno recuperate 463869 righe con l'indice, la vera stima in questo caso è il numero di esecuzioni (2) * 463869 = 927738 che non è così lontano dal numero effettivo di righe, 1391608. Sorprendentemente, il numero di righe stimate è quasi perfetto immediatamente dopo il join del ciclo nidificato nell'ID nodo 10.

Stime di cardinalità scadenti sono per lo più un problema quando Query Optimizer sceglie il piano sbagliato o non concede memoria sufficiente al piano. Non vedo versamenti su tempdb per questo piano, quindi la memoria sembra a posto. Per il join di loop nidificato che si chiama, si dispone di una tabella esterna piccola e di una tabella interna indicizzata. Cosa c'è che non va? Per essere precisi, cosa ti aspetteresti che Query Optimizer faccia diversamente qui?

In termini di miglioramento delle prestazioni, la cosa che mi distingue è che SQL Server sta usando un algoritmo di hashing per distribuire righe parallele che si traducono in tutte le stesse nello stesso thread:

squilibrio del filo

Di conseguenza, un thread fa tutto il lavoro con l'indice cerca:

ricerca dello squilibrio del filo

Ciò significa che la query non viene effettivamente eseguita in parallelo fino a quando l'operatore di ripartizione non esegue lo streaming dell'operatore al nodo ID 9. Ciò che probabilmente si desidera è il partizionamento round robin in modo che ogni riga finisca sul proprio thread. Ciò consentirà a due thread di eseguire la ricerca dell'indice per l'id nodo 17. Aggiunta di un superfluoTOP operatore può farti partizionare round robin. Posso aggiungere dettagli qui se vuoi.

Se vuoi davvero concentrarti sulle stime della cardinalità, puoi inserire le righe dopo il primo join in una tabella temporanea. Se si raccolgono statistiche sulla tabella temporanea che fornisce all'ottimizzatore ulteriori informazioni sulla tabella esterna per il join del ciclo nidificato che è stato richiamato. Potrebbe anche provocare il partizionamento round robin.

Se non si utilizzano i flag di traccia 4199 o 2301, è possibile considerarli. Trace flag 4199 offre un'ampia varietà di correzioni per l'ottimizzatore, ma possono ridurre alcuni carichi di lavoro. Il flag di traccia 2301 modifica alcune delle ipotesi di cardinalità di join di Query Optimizer e lo rende più difficile. In entrambi i casi testare attentamente prima di abilitarli.


-2

Credo che ottenere una stima migliore su quell'unione non cambierà il piano, a meno che 1.4 mill non sia una porzione sufficiente della tabella per fare in modo che l'ottimizzatore scelga una scansione di indice (non cluster) con hash o unione unita. Ho il sospetto che non sarebbe il caso qui, né effettivamente utile, ma puoi testare gli effetti sostituendo il join interno con CustomAttributeValues ​​con il join hash interno e il join unione interno .

Ho anche esaminato il codice in modo più ampio e non vedo alcun modo per migliorarlo - sarei interessato a essere smentito, ovviamente. E se hai voglia di pubblicare la logica completa di ciò che stai cercando di realizzare, sarei interessato a un altro aspetto.


3
Esiste uno spazio molto ampio di piani per quella query, con molte opzioni per ordine e nidificazione dei join, parallelismo, aggregazione locale / globale ecc. Ecc., La maggior parte dei quali sarebbe influenzata da cambiamenti nelle statistiche derivate (distribuzione e cardinalità grezza) nel nodo piano 10. Notare anche che i suggerimenti sui join dovrebbero essere generalmente evitati poiché sono dotati di un silent OPTION(FORCE ORDER), che impedisce all'ottimizzatore di riordinare i join dalla sequenza testuale e molte altre ottimizzazioni oltre.
Paul White 9

-12

Non migliorerai da una ricerca indice [non raggruppata]. L'unica cosa migliore di una ricerca di indice non cluster è una ricerca di indice cluster.

Inoltre, sono stato un DBA SQL negli ultimi dieci anni e uno sviluppatore SQL per cinque anni prima, e nella mia esperienza è estremamente raro trovare un miglioramento a una query SQL studiando il piano di esecuzione che non è stato possibile trovare con altri mezzi. Il motivo principale per generare il piano di esecuzione è perché spesso ti suggeriranno indici mancanti che puoi aggiungere per migliorare le prestazioni.

I principali miglioramenti delle prestazioni saranno nella regolazione della query SQL stessa, se vi sono inefficienze. Ad esempio, un paio di mesi fa ho ottenuto una funzione SQL da eseguire 160 volte più veloce riscrivendo una SELECT UNION SELECTtabella pivot di stile per utilizzare l' PIVOToperatore SQL standard .

insert into Variable1 values (?), (?), (?)


select *
    into Object1
    from Variable2
        where Column1 is not null;



select Variable3 = Function1(distinct Column2) 
    from Object2 Object3
        inner join Object1 Object4 on Object3.Column1 = Object4.Column1;



select Object4.Column1
        , Object4.Column3 
    from Object5 Object4
        inner join Object6 Object7
            on Object4.Column1 = Object7.Column4
        inner join Object2 Object8 
            on Object8.Column1 = Object7.Column5
    where Object4.Column6 = Variable4
        and Object7.Column5 in (select Column1 from Object1)
    group by Object4.Column3
        , Object4.Column1
    having Function1(distinct Object8.Column2) = Variable3
    option(recompile);

Vediamo, SELECT * INTOgeneralmente è meno efficiente di uno standard INSERT Object1 (column list) SELECT column list. Quindi lo riscriverei. Successivamente, se Function1 è stato definito senza a WITH SCHEMABINDING, l'aggiunta di una WITH SCHEMABINDINGclausola dovrebbe consentirgli di eseguire più velocemente.

Hai scelto molti alias che non hanno senso, come l'aliasing Object2 come Object3. Dovresti scegliere alias migliori che non offuscano il codice. Hai "Object7.Column5 in (seleziona Column1 da Object1)".

INclausole di questo tipo sono sempre più efficienti scritte come EXISTS (SELECT 1 FROM Object1 o1 WHERE o1.Column1 = Object7.Column5). Forse avrei dovuto scriverlo nell'altro modo. EXISTSsarà sempre almeno buono come IN. Non è sempre meglio, ma di solito lo è.

Inoltre, dubito che qui option(recompile)stia migliorando le prestazioni della query. Proverei a rimuoverlo.


6
Se una ricerca di indice non cluster copre la query, sarà quasi sempre migliore di una ricerca di indice cluster, poiché per definizione, l'indice di cluster ha tutte le colonne al suo interno e l'indice non di cluster ha un numero inferiore di colonne, quindi richiederà meno ricerche di pagina (e meno livelli di passaggi nel b-tree) per recuperare i dati. Quindi non è accurato dire che una ricerca di indici cluster sarà sempre migliore.
ErikE,
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.