Perché la selezione di tutte le colonne risultanti di questa query è più rapida della selezione di una colonna a cui tengo?


13

Ho una domanda in cui l'utilizzo select *non solo fa molto meno letture, ma utilizza anche significativamente meno tempo della CPU rispetto all'utilizzo select c.Foo.

Questa è la domanda:

select top 1000 c.ID
from ATable a
    join BTable b on b.OrderKey = a.OrderKey and b.ClientId = a.ClientId
    join CTable c on c.OrderId = b.OrderId and c.ShipKey = a.ShipKey
where (a.NextAnalysisDate is null or a.NextAnalysisDate < @dateCutOff)
    and b.IsVoided = 0
    and c.ComplianceStatus in (3, 5)
    and c.ShipmentStatus in (1, 5, 6)
order by a.LastAnalyzedDate

Ciò terminò con 2.473.658 letture logiche, principalmente nella Tabella B. Utilizzava 26.562 CPU e aveva una durata di 7.965.

Questo è il piano di query generato:

Pianifica dalla selezione del valore di una singola colonna Su PasteThePlan: https://www.brentozar.com/pastetheplan/?id=BJAp2mQIQ

Quando cambio c.IDa *, la query è terminata con 107.049 letture logiche, distribuite in modo abbastanza uniforme tra tutte e tre le tabelle. Utilizzava 4.266 CPU e aveva una durata di 1.147.

Questo è il piano di query generato:

Pianifica da Selezione di tutti i valori Su PasteThePlan: https://www.brentozar.com/pastetheplan/?id=SyZYn7QUQ

Ho provato a utilizzare i suggerimenti per le query suggeriti da Joe Obbish, con questi risultati:
select c.IDsenza suggerimento: https://www.brentozar.com/pastetheplan/?id=SJfBdOELm
select c.ID con suggerimento: https://www.brentozar.com/pastetheplan/ ? id = B1W ___ N87
select * senza suggerimento: https://www.brentozar.com/pastetheplan/?id=HJ6qddEIm
select * con suggerimento: https://www.brentozar.com/pastetheplan/?id=rJhhudNIQ

L'uso del OPTION(LOOP JOIN)suggerimento con select c.IDha ridotto drasticamente il numero di letture rispetto alla versione senza suggerimento, ma sta ancora facendo circa il doppio del numero di letture della select *query senza alcun suggerimento. L'aggiunta OPTION(RECOMPILE, HASH JOIN)alla select *query ha reso le prestazioni molto peggiori di qualsiasi altra cosa che abbia provato.

Dopo aver aggiornato le statistiche sulle tabelle e sui loro indici utilizzando WITH FULLSCAN, la select c.IDquery viene eseguita molto più velocemente:
select c.IDprima dell'aggiornamento: https://www.brentozar.com/pastetheplan/?id=SkiYoOEUm
select * prima dell'aggiornamento: https://www.brentozar.com/ pastetheplan /? id = ryrvodEUX
select c.ID dopo l'aggiornamento: https://www.brentozar.com/pastetheplan/?id=B1MRoO487
select * dopo l'aggiornamento: https://www.brentozar.com/pastetheplan/?id=Hk7si_V8m

select *supera ancora select c.IDin termini di durata totale e letture totali ( select *ha circa la metà delle letture) ma utilizza più CPU. Nel complesso sono molto più vicini rispetto a prima dell'aggiornamento, tuttavia i piani sono ancora diversi.

Lo stesso comportamento si riscontra nel 2016 in esecuzione nella modalità di compatibilità 2014 e nel 2014. Cosa potrebbe spiegare la disparità tra i due piani? Potrebbe essere che gli indici "corretti" non siano stati creati? Le statistiche che potrebbero essere leggermente obsolete potrebbero causare questo?

Ho provato a spostare i predicati fino alla ONparte del join, in diversi modi, ma il piano di query è lo stesso ogni volta.

Dopo la ricostruzione dell'indice

Ho ricostruito tutti gli indici sulle tre tabelle coinvolte nella query. c.IDsta ancora eseguendo il maggior numero di letture (oltre il doppio *), ma l'utilizzo della CPU è circa la metà della *versione. La c.IDversione anche riversata in tempdb sul smistamento di ATable:
c.ID: https://www.brentozar.com/pastetheplan/?id=HyHIeDO87
* : https://www.brentozar.com/pastetheplan/?id=rJ4deDOIQ

Ho anche provato a forzarlo a funzionare senza parallelismo e questo mi ha dato la query con le migliori prestazioni: https://www.brentozar.com/pastetheplan/?id=SJn9-vuLX

Vedo il numero di esecuzioni degli operatori DOPO la ricerca del grande indice che sta eseguendo l'ordinamento eseguito solo 1.000 volte nella versione a thread singolo, ma ha fatto significativamente di più nella versione parallela, tra 2.622 e 4.315 esecuzioni di vari operatori.

Risposte:


4

È vero che la selezione di più colonne implica che SQL Server potrebbe dover lavorare di più per ottenere i risultati richiesti della query. Se Query Optimizer fosse in grado di elaborare il piano di query perfetto per entrambe le query, sarebbe ragionevole aspettarsi cheSELECT *la query verrà eseguita più a lungo della query che seleziona tutte le colonne da tutte le tabelle. Hai osservato il contrario per la tua coppia di domande. È necessario fare attenzione quando si confrontano i costi, ma la query lenta ha un costo totale stimato di unità di ottimizzazione 1090.08 e la query veloce ha un costo totale stimato di unità di ottimizzazione 6823.11. In questo caso, si potrebbe dire che l'ottimizzatore fa un cattivo lavoro con la stima dei costi totali delle query. Ha scelto un piano diverso per la tua query SELECT * e si aspettava che quel piano fosse più costoso, ma non era così. Questo tipo di mancata corrispondenza può verificarsi per molte ragioni e una delle cause più comuni è la stima della cardinalità. I costi dell'operatore sono in gran parte determinati dalle stime della cardinalità. Se una stima della cardinalità in un punto chiave di un piano è imprecisa, il costo totale del piano potrebbe non riflettere la realtà. Questa è una semplificazione eccessiva, ma spero che possa essere utile per capire cosa sta succedendo qui.

Cominciamo discutendo perché una SELECT *query potrebbe essere più costosa della selezione di una singola colonna. La SELECT *query potrebbe trasformare alcuni indici di copertura in indici non di copertura, il che potrebbe significare che l'ottimizzatore deve fare un lavoro di addizione per ottenere tutte le colonne di cui ha bisogno o potrebbe essere necessario leggere da un indice più grande.SELECT *può anche comportare set di risultati intermedi più grandi che devono essere elaborati durante l'esecuzione della query. Puoi vederlo in azione osservando le dimensioni delle righe stimate in entrambe le query. Nella query veloce le dimensioni delle righe variano da 664 byte a 3019 byte. Nella query lenta le dimensioni delle righe variano da 19 a 36 byte. Il blocco di operatori come ordinamenti o build di hash comporta costi più elevati per i dati con dimensioni di riga maggiori poiché SQL Server sa che è più costoso ordinare grandi quantità di dati o trasformarli in una tabella di hash.

Guardando la query veloce, l'ottimizzatore stima che occorra cercare 2,4 milioni di ricerche sull'indice Database1.Schema1.Object5.Index3. Ecco da dove viene la maggior parte del costo del piano. Tuttavia, il piano attuale rivela che su quell'operatore sono state eseguite solo 1332 ricerche di indici. Se si confrontano le righe effettive con quelle stimate per le parti esterne di quei join di loop, si noteranno grandi differenze. L'ottimizzatore ritiene che saranno necessarie molte più ricerche di indice per trovare le prime 1000 righe necessarie per i risultati della query. Ecco perché la query ha un piano di costi relativamente elevato ma termina così rapidamente: l'operatore che si riteneva il più costoso ha svolto meno dello 0,1% del lavoro previsto.

Osservando la query lenta, si ottiene un piano per lo più con hash join (credo che il loop loop sia lì solo per gestire la variabile locale). Le stime della cardinalità sicuramente non sono perfette, ma l'unico vero problema di stima è proprio alla fine con l'ordinamento. Sospetto che la maggior parte del tempo sia dedicato alle scansioni dei tavoli con centinaia di milioni di righe.

Potrebbe essere utile aggiungere suggerimenti per le query a entrambe le versioni della query per forzare il piano di query associato all'altra versione. I suggerimenti per le query possono essere un buon strumento per capire perché l'ottimizzatore ha fatto alcune delle sue scelte. Se aggiungi OPTION (RECOMPILE, HASH JOIN)alla SELECT *query mi aspetto che vedrai un piano di query simile alla query di hash join. Mi aspetto inoltre che i costi delle query saranno molto più elevati per il piano di join hash perché le dimensioni delle righe sono molto più grandi. Quindi questo potrebbe essere il motivo per cui la query di hash join non è stata scelta per la SELECT *query. Se aggiungi OPTION (LOOP JOIN)alla query che seleziona solo una colonna mi aspetto che vedrai un piano di query simile a quello per ilSELECT *query. In questo caso, la riduzione delle dimensioni della riga non dovrebbe avere un grande impatto sul costo complessivo della query. Potresti saltare le ricerche chiave ma questa è una piccola percentuale del costo stimato.

In sintesi, mi aspetto che le dimensioni delle righe più grandi necessarie per soddisfare la SELECT *query spingano l'ottimizzatore verso un piano di join loop anziché un piano di join hash. Il piano di join loop ha un costo superiore a quello che dovrebbe essere dovuto a problemi di stima della cardinalità. Ridurre le dimensioni delle righe selezionando solo una colonna riduce notevolmente il costo di un piano di join hash ma probabilmente non avrà un grande effetto sul costo di un piano di join loop, quindi si finisce con il piano di join hash meno efficiente. È difficile dire altro per un piano anonimo.


Grazie mille per la tua risposta espansiva e istruttiva. Ho provato ad aggiungere i suggerimenti che hai suggerito. Ha reso la select c.IDquery molto più veloce, ma sta ancora facendo un lavoro extra che la select *query, senza suggerimenti, fa.
L. Miller,

2

Le statistiche obsolete possono certamente indurre l'ottimizzatore a scegliere un metodo scadente per trovare i dati. Hai provato a fare un UPDATE STATISTICS ... WITH FULLSCANo fare un pieno REBUILDsull'indice? Provalo e vedi se aiuta.

AGGIORNARE

Secondo un aggiornamento dall'OP:

Dopo aver aggiornato le statistiche sulle tabelle e sui loro indici utilizzando WITH FULLSCAN, la select c.IDquery viene eseguita molto più velocemente

Quindi, ora, se l'unica azione intrapresa fosse UPDATE STATISTICS, allora prova a fare un indice REBUILD(non REORGANIZE) come ho visto quell'aiuto con il conteggio delle righe stimato, dove entrambi UPDATE STATISTICSe l'indice REORGANIZEno.


Sono stato in grado di ottenere tutti gli indici delle tre tabelle coinvolte da ricostruire nel fine settimana e ho aggiornato il mio post per riflettere quei risultati.
L. Miller,

-1
  1. Potete per favore includere gli script dell'indice?
  2. Hai eliminato possibili problemi con lo "sniffing dei parametri"? https://www.mssqltips.com/sqlservertip/3257/different-approaches-to-correct-sql-server-parameter-sniffing/
  3. Ho trovato questa tecnica utile in alcuni casi:
    a) riscrivere ogni tabella come una sottoquery, seguendo queste regole:
    b) SELEZIONA - metti prima le colonne di join
    c) PREDICATI - sposta nelle rispettive sottoquery
    d) ORDINA - sposta nelle loro rispettive subquery, ordina su UNISCI COLONNE PRIMA
    e) Aggiungi una query wrapper per l'ordinamento finale e SELEZIONA.

L'idea è di preordinare le colonne di join all'interno di ciascuna sottoselezione, mettendo le colonne di join prima in ciascun elenco di selezione.

Ecco cosa intendo ....

SELECT ... wrapper query
FROM
(
    SELECT ...
    FROM
        (SELECT ClientID, ShipKey, NextAnalysisDate
         FROM ATABLE
         WHERE (a.NextAnalysisDate is null or a.NextAnalysisDate < @dateCutOff) -- Predicates
         ORDER BY OrderKey, ClientID, LastAnalyzedDate  ---- Pre-sort the join columns
        ) as a
        JOIN 
        (SELECT OrderKey, ClientID, OrderID, IsVoided
         FROM BTABLE
         WHERE IsVoided = 0             ---- Include all predicates
         ORDER BY OrderKey, OrderID, IsVoided       ---- Pre-sort the join columns
        ) as b ON b.OrderKey = a.OrderKey and b.ClientId = a.ClientId
        JOIN
        (SELECT OrderID, ShipKey, ComplianceStatus, ShipmentStatus, ID
         FROM CTABLE
         WHERE ComplianceStatus in (3, 5)       ---- Include all predicates
             AND ShipmentStatus in (1, 5, 6)        ---- Include all predicates
         ORDER BY OrderID, ShipKey          ---- Pre-sort the join columns
        ) as c ON c.OrderId = b.OrderId and c.ShipKey = a.ShipKey
) as d
ORDER BY d.LastAnalyzedDate

1
1. Proverò ad aggiungere script DDL indice al post originale, che potrebbe richiedere del tempo per "scrub". 2. Ho verificato questa possibilità sia cancellando la cache del piano prima dell'esecuzione, sia sostituendo il parametro bind con un valore effettivo. 3. Ho ORDER BYprovato a farlo , ma non è valido in una sottoquery senza TOP, FORXML, ecc. L'ho provato senza le ORDER BYclausole ma era lo stesso piano.
L. Miller,
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.