Perché il costo stimato di (le stesse) 1000 ricerche su un indice unico differisce in questi piani?


28

Nelle query sottostanti si stima che entrambi i piani di esecuzione eseguano 1.000 ricerche su un indice univoco.

Le ricerche sono guidate da una scansione ordinata sulla stessa tabella di origine, quindi apparentemente dovrebbe finire per cercare gli stessi valori nello stesso ordine.

Entrambi i loop nidificati hanno <NestedLoops Optimized="false" WithOrderedPrefetch="true">

Qualcuno sa perché questa attività è costata a 0,172434 nel primo piano ma a 3,01702 nel secondo?

(Il motivo della domanda è che la prima query mi è stata suggerita come un'ottimizzazione a causa del costo del piano apparentemente molto più basso. In realtà mi sembra che faccia più lavoro ma sto solo cercando di spiegare la discrepanza .. .)

Impostare

CREATE TABLE dbo.Target(KeyCol int PRIMARY KEY, OtherCol char(32) NOT NULL);

CREATE TABLE dbo.Staging(KeyCol int PRIMARY KEY, OtherCol char(32) NOT NULL); 

INSERT INTO dbo.Target
SELECT TOP (1000000) ROW_NUMBER() OVER (ORDER BY @@SPID), LEFT(NEWID(),32)
FROM master..spt_values v1,  
     master..spt_values v2;

INSERT INTO dbo.Staging
SELECT TOP (1000) ROW_NUMBER() OVER (ORDER BY @@SPID), LEFT(NEWID(),32)
FROM master..spt_values v1;

Interrogazione 1 collegamento "Incolla il piano"

WITH T
     AS (SELECT *
         FROM   Target AS T
         WHERE  T.KeyCol IN (SELECT S.KeyCol
                             FROM   Staging AS S))
MERGE T
USING Staging S
ON ( T.KeyCol = S.KeyCol )
WHEN NOT MATCHED THEN
  INSERT ( KeyCol, OtherCol )
  VALUES(S.KeyCol, S.OtherCol )
WHEN MATCHED AND T.OtherCol > S.OtherCol THEN
  UPDATE SET T.OtherCol = S.OtherCol;

Interrogazione 2 collegamento "Incolla il piano"

MERGE Target T
USING Staging S
ON ( T.KeyCol = S.KeyCol )
WHEN NOT MATCHED THEN
  INSERT ( KeyCol, OtherCol )
  VALUES( S.KeyCol, S.OtherCol )
WHEN MATCHED AND T.OtherCol > S.OtherCol THEN
  UPDATE SET T.OtherCol = S.OtherCol; 

Query 1

Query 2

Quanto sopra è stato testato su SQL Server 2014 (SP2) (KB3171021) - 12.0.5000.0 (X64)


@Joe Obbish sottolinea nei commenti che sarebbe una riproduzione più semplice

SELECT *
FROM staging AS S 
  LEFT OUTER JOIN Target AS T 
    ON T.KeyCol = S.KeyCol;

vs

SELECT *
FROM staging AS S 
  LEFT OUTER JOIN (SELECT * FROM Target) AS T 
    ON T.KeyCol = S.KeyCol;

Per la tabella di gestione temporanea a 1.000 righe, entrambe le precedenti hanno ancora la stessa forma di piano con loop nidificati e il piano senza la tabella derivata appare più economico, ma per una tabella di gestione temporanea a 10.000 righe e la stessa tabella di destinazione sopra, la differenza di costi modifica il piano la forma (con una scansione completa e unisci unione che sembra relativamente più attraente di ricerche a costo elevato) che mostra questa discrepanza di costo può avere implicazioni oltre a rendere più difficile il confronto dei piani.

inserisci qui la descrizione dell'immagine

Risposte:


21

Qualcuno sa perché questa attività è costata a 0,172434 nel primo piano ma a 3,01702 nel secondo?

In generale, un lato interno cerca al di sotto di un'unione di anelli nidificati viene valutato assumendo uno schema I / O casuale. Esiste una semplice riduzione basata sulla sostituzione per gli accessi successivi, tenendo conto della possibilità che la pagina richiesta sia già stata portata in memoria da una precedente iterazione. Questa valutazione di base produce il costo standard (più elevato).

C'è un altro input di costo, Smart Seek Costing , di cui si conoscono pochi dettagli. La mia ipotesi (e questo è tutto ciò che è in questa fase) è che SSC tenta di valutare il costo di I / O della ricerca della parte interna in modo più dettagliato, forse considerando l'ordinamento locale e / o l'intervallo di valori da recuperare. Chissà.

Ad esempio, la prima operazione di ricerca porta non solo la riga richiesta, ma tutte le righe su quella pagina (in ordine di indice). Dato il modello di accesso complessivo, il recupero delle 1000 righe in 1000 ricerche richiede solo 2 letture fisiche, anche con read-ahead e prefetching disabilitati. Da quel punto di vista, il costo I / O predefinito rappresenta una sovrastima significativa e il costo corretto per SSC è più vicino alla realtà.

Sembra ragionevole aspettarsi che SSC sia più efficace laddove il loop spinga un indice a cercare più o meno direttamente e il riferimento esterno del join è la base dell'operazione di ricerca. Da quello che posso dire, SSC è sempre tentato per operazioni fisiche adeguate, ma molto spesso non produce aggiustamenti verso il basso quando la ricerca è separata dal join da altre operazioni. I filtri semplici sono un'eccezione a questo, forse perché SQL Server può spesso inserirli nell'operatore di accesso ai dati. In ogni caso, l'ottimizzatore ha un supporto abbastanza profondo per le selezioni.

È un peccato che lo scalare di calcolo per le proiezioni esterne di subquery sembra interferire con SSC qui. Gli scalari di calcolo vengono solitamente trasferiti sopra il join, ma questi devono rimanere dove sono. Anche così, gli scalari di calcolo più normali sono abbastanza trasparenti per l'ottimizzazione, quindi questo è un po 'sorprendente.

Indipendentemente da ciò, quando l'operazione fisica PhyOp_Rangeè prodotta da una semplice selezione su un indice SelIdxToRng, SSC è efficace. Quando viene utilizzato il più complesso SelToIdxStrategy(selezione su una tabella per una strategia di indice), il risultante PhyOp_Rangeesegue SSC ma non comporta alcuna riduzione. Ancora una volta, sembra che le operazioni più semplici e dirette funzionino meglio con SSC.

Vorrei poterti dire esattamente cosa fa SSC e mostrare i calcoli esatti, ma non conosco quei dettagli. Se si desidera esplorare l'output di traccia limitato disponibile per sé, è possibile utilizzare il flag di traccia non documentato 2398. Un esempio di output è:

Costo ricerca intelligente (7.1) :: 1.34078e + 154, 0,001

Quell'esempio si riferisce al gruppo di memo 7, alternativa 1, che mostra un limite superiore di costo e un fattore di 0,001. Per vedere i fattori più puliti, assicurati di ricostruire le tabelle senza parallelismo in modo che le pagine siano il più fitte possibile. Senza farlo, il fattore è più simile a 0.000821 per la tabella Target di esempio. Ci sono alcune relazioni abbastanza ovvie lì, ovviamente.

SSC può anche essere disabilitato con flag di traccia non documentato 2399. Con quel flag attivo, entrambi i costi hanno il valore più alto.


8

Non sono sicuro che questa sia una risposta, ma è un po 'lunga per un commento. La causa della differenza è la pura speculazione da parte mia e forse può essere cibo per il pensiero degli altri.

Query semplificate con piani di esecuzione.

SELECT S.KeyCol, 
       S.OtherCol,
       T.*
FROM staging AS S 
  LEFT OUTER JOIN Target AS T 
    ON T.KeyCol = S.KeyCol;

SELECT S.KeyCol, 
       S.OtherCol,
       T.*
FROM staging AS S 
  LEFT OUTER JOIN (
                  SELECT *
                  FROM Target
                  ) AS T 
    ON T.KeyCol = S.KeyCol;

inserisci qui la descrizione dell'immagine

La principale differenza tra queste query equivalenti che potrebbero effettivamente portare a piani di esecuzione identici è l'operatore scalare di calcolo. Non so perché debba essere lì, ma suppongo che l'ottimizzatore possa arrivare a ottimizzare la tabella derivata.

La mia ipotesi è che la presenza dello scalare di calcolo sia ciò che sta riducendo il costo di I / O per la seconda query.

Dall'interno dell'ottimizzatore: pianificare i costi

Il costo della CPU viene calcolato come 0,0001581 per la prima riga e 0,000011 per le righe successive.
...
Il costo I / O di 0,003125 è esattamente 1/320 - riflettendo il presupposto del modello che il sottosistema disco può eseguire 320 operazioni I / O casuali al secondo
...
il componente di costing è abbastanza intelligente da riconoscere che il numero totale di le pagine che devono essere introdotte dal disco non possono mai superare il numero di pagine richieste per memorizzare l'intera tabella.

Nel mio caso la tabella impiega 5618 pagine e per ottenere 1000 righe da 1000000 righe il numero stimato di pagine necessarie è 5,618, con un costo di I / O di 0,015625.

Il costo della CPU per entrambe le cuciture di query deve essere lo stesso 0.0001581 * 1000 executions = 0.1581,.

Quindi, secondo l'articolo collegato sopra, possiamo calcolare il costo per la prima query pari a 0,173725.

E supponendo che io abbia ragione su come lo scalare di calcolo stia facendo un casino di IO Cost, può essere calcolato a 3.2831.

Non esattamente ciò che viene mostrato nei piani, ma è proprio lì nel quartiere.


6

(Sarebbe meglio come commento alla risposta di Paul, ma non ho ancora abbastanza rappresentante.)

Volevo fornire un elenco di flag di traccia (e un paio di DBCCdichiarazioni) che ero solito giungere a una conclusione quasi esaurita, nel caso in cui sarà utile indagare discrepanze simili in futuro. Tutti questi non dovrebbero essere usati sulla produzione .

Innanzitutto, ho dato un'occhiata al memo finale per vedere quali operatori fisici venivano utilizzati. Sicuramente sembrano uguali secondo i piani grafici di esecuzione. Quindi, ho usato i flag di traccia 3604e 8615, il primo dirige l'output al client e il secondo rivela il memo finale:

SELECT S.*, T.KeyCol
FROM Staging AS S
      LEFT OUTER JOIN Target AS T
       ON T.KeyCol = S.KeyCol
OPTION(QUERYTRACEON 3604, -- Output client info
       QUERYTRACEON 8615, -- Shows Final Memo structure
       RECOMPILE);

Tracciando da Root Group, ho trovato questi PhyOp_Rangeoperatori quasi identici :

  1. PhyOp_Range 1 ASC 2.0 Cost(RowGoal 0,ReW 0,ReB 999,Dist 1000,Total 1000)= 0.175559(Distance = 2)
  2. PhyOp_Range 1 ASC 3.0 Cost(RowGoal 0,ReW 0,ReB 999,Dist 1000,Total 1000)= 3.01702(Distance = 2)

L'unica ovvia differenza per me era la 2.0e 3.0, che si riferiscono ai rispettivi "gruppo memo 2, originale" e "gruppo memo 3, originale". Controllando il promemoria, si riferiscono alla stessa cosa, quindi non sono state ancora rilevate differenze.

In secondo luogo, ho esaminato un intero casino di tracce di traccia che si sono rivelate inutili per me - ma hanno alcuni contenuti interessanti. Ho sollevato la maggior parte da Benjamin Nevarez . Stavo cercando indizi sulle regole di ottimizzazione applicate in un caso e non nell'altro.

 SELECT S.*, T.KeyCol
 FROM Staging AS S
      LEFT OUTER JOIN Target AS T
        ON T.KeyCol = S.KeyCol
 OPTION (QUERYTRACEON 3604, -- Output info to client
         QUERYTRACEON 2363, -- Show stats and cardinality info
         QUERYTRACEON 8675, -- Show optimization process info
         QUERYTRACEON 8606, -- Show logical query trees
         QUERYTRACEON 8607, -- Show physical query tree
         QUERYTRACEON 2372, -- Show memory utilization info for optimization stages 
         QUERYTRACEON 2373, -- Show memory utilization info for applying rules
         RECOMPILE );

In terzo luogo, ho esaminato quali regole sono state applicate per i nostri PhyOp_Rangeche sembrano così simili. Ho usato un paio di flag di traccia menzionati da Paul in un post sul blog .

SELECT S.*, T.KeyCol
FROM Staging AS S
      LEFT OUTER JOIN (SELECT KeyCol
                      FROM Target) AS T
       ON T.KeyCol = S.KeyCol
OPTION (QUERYTRACEON 3604, -- Output info to client
        QUERYTRACEON 8619, -- Show applied optimization rules
        QUERYTRACEON 8620, -- Show rule-to-memo info
        QUERYTRACEON 8621, -- Show resulting tree
        QUERYTRACEON 2398, -- Show "smart seek costing"
        RECOMPILE );

Dalla uscita, vediamo che il diretta- JOINapplicato questa regola per ottenere il nostro PhyOp_Rangeoperatore: Rule Result: group=7 2 <SelIdxToRng>PhyOp_Range 1 ASC 2 (Distance = 2). La subselect applicata questa regola, invece: Rule Result: group=9 2 <SelToIdxStrategy>PhyOp_Range 1 ASC 3 (Distance = 2). Questo è anche il punto in cui vengono visualizzate le informazioni sul "costo di ricerca intelligente" associate a ciascuna regola. Per la diretta- JOINquesto è l'uscita (per me): Smart seek costing (7.2) :: 1.34078e+154 , 0.001. Per la subselect, questo è l'output: Smart seek costing (9.2) :: 1.34078e+154 , 1.

Alla fine, non sono riuscito a concludere molto, ma la risposta di Paul colma la maggior parte del divario. Mi piacerebbe vedere qualche informazione in più sui costi di ricerca intelligente.


4

Neanche questa è una risposta - come ha notato Mikael, è difficile discutere questo problema nei commenti ...

È interessante notare che se si converte la sottoquery (select KeyCol FROM Target)in un TVF in linea, si vede il piano e i suoi costi sono gli stessi della semplice query originale:

CREATE FUNCTION dbo.cs_test()
RETURNS TABLE
WITH SCHEMABINDING
AS 
RETURN (
    SELECT KeyCol FROM dbo.Target
    );

/* "normal" variant */
SELECT S.KeyCol, s.OtherCol, T.KeyCol 
FROM staging AS S 
    LEFT OUTER JOIN Target AS T ON T.KeyCol = S.KeyCol;

/* "subquery" variant */
SELECT S.KeyCol, s.OtherCol, T.KeyCol 
FROM staging AS S 
    LEFT OUTER JOIN (SELECT KeyCol FROM Target) AS T ON T.KeyCol = S.KeyCol;

/* "inline-TVF" variant */
SELECT S.KeyCol, s.OtherCol, T.KeyCol 
FROM staging AS S 
    LEFT OUTER JOIN dbo.cs_test() t ON s.KeyCol = t.Keycol

I piani di query ( collegamento pastetheplan ):

inserisci qui la descrizione dell'immagine

La deduzione mi porta a credere che il motore dei costi sia confuso circa il potenziale impatto che questo tipo di subquery può avere .

Prendiamo ad esempio quanto segue:

SELECT S.KeyCol, s.OtherCol, T.KeyCol 
FROM staging AS S 
    LEFT OUTER JOIN (
        SELECT KeyCol = CHECKSUM(NEWID()) 
        FROM Target
        ) AS T ON T.KeyCol = S.KeyCol;

In che modo si costare questo? Query Optimizer sceglie un piano molto simile alla variante "subquery" sopra, contenente uno scalare di calcolo ( link pastetheplan.com ):

inserisci qui la descrizione dell'immagine

Lo scalare di calcolo ha un costo abbastanza diverso dalla variante "subquery" mostrata sopra, tuttavia è ancora semplicemente un'ipotesi poiché l'ottimizzatore di query non ha modo di sapere, a priori, quale potrebbe essere il numero di righe restituite. Il piano utilizza una corrispondenza hash per il join esterno sinistro poiché le stime delle righe sono inconoscibili e pertanto impostate sul numero di righe nella tabella Target.

inserisci qui la descrizione dell'immagine

Non ho una grande conclusione da ciò se non che sono d'accordo con il lavoro svolto da Mikael nella sua risposta, e spero che qualcun altro possa trovare una risposta migliore.

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.