Perché ci sono differenze nel piano di esecuzione tra OFFSET ... FETCH e lo schema ROW_NUMBER vecchio stile?


15

Il nuovo OFFSET ... FETCHmodello introdotto con SQL Server 2012 offre un paging semplice e veloce. Perché ci sono delle differenze considerando che le due forme sono semanticamente identiche e molto comuni?

Si potrebbe presumere che l'ottimizzatore riconosca entrambi e li ottimizzi (banalmente) al massimo.

Ecco un caso molto semplice in cui OFFSET ... FETCHè ~ 2x più veloce in base alla stima dei costi.

SELECT * INTO #objects FROM sys.objects

SELECT *
FROM (
    SELECT *, ROW_NUMBER() OVER (ORDER BY object_id) r
    FROM #objects
) x
WHERE r >= 30 AND r < (30 + 10)
    ORDER BY object_id

SELECT *
FROM #objects
ORDER BY object_id
OFFSET 30 ROWS FETCH NEXT 10 ROWS ONLY

offset fetch.png

È possibile variare questo caso di test creando un elemento della configurazione object_ido aggiungendo filtri, ma è impossibile rimuovere tutte le differenze di piano. OFFSET ... FETCHè sempre più veloce perché fa meno lavoro al momento dell'esecuzione.


Non molto sicuro, quindi inserendolo come commento, ma immagino sia perché hai lo stesso ordine per condizione per la numerazione delle righe e il set di risultati finali. Poiché nella seconda condizione, l'ottimizzatore lo sa, non è necessario riordinare i risultati. Nel primo caso, tuttavia, è necessario assicurarsi che i risultati della selezione esterna siano ordinati e la numerazione delle righe nel risultato interno. La creazione di un indice corretto su #objects dovrebbe risolvere il problema
Akash,

Risposte:


13

Gli esempi nella domanda non producono esattamente gli stessi risultati (l' OFFSETesempio ha un errore off-by-one). I moduli aggiornati di seguito risolvono il problema, rimuovono l'ordinamento aggiuntivo per il ROW_NUMBERcaso e utilizzano le variabili per rendere la soluzione più generale:

DECLARE 
    @PageSize bigint = 10,
    @PageNumber integer = 3;

WITH Numbered AS
(
    SELECT TOP ((@PageNumber + 1) * @PageSize) 
        o.*,
        rn = ROW_NUMBER() OVER (
            ORDER BY o.[object_id])
    FROM #objects AS o
    ORDER BY 
        o.[object_id]
)
SELECT
    x.name,
    x.[object_id],
    x.principal_id,
    x.[schema_id],
    x.parent_object_id,
    x.[type],
    x.type_desc,
    x.create_date,
    x.modify_date,
    x.is_ms_shipped,
    x.is_published,
    x.is_schema_published
FROM Numbered AS x
WHERE
    x.rn >= @PageNumber * @PageSize
    AND x.rn < ((@PageNumber + 1) * @PageSize)
ORDER BY
    x.[object_id];

SELECT
    o.name,
    o.[object_id],
    o.principal_id,
    o.[schema_id],
    o.parent_object_id,
    o.[type],
    o.type_desc,
    o.create_date,
    o.modify_date,
    o.is_ms_shipped,
    o.is_published,
    o.is_schema_published
FROM #objects AS o
ORDER BY 
    o.[object_id]
    OFFSET @PageNumber * @PageSize - 1 ROWS 
    FETCH NEXT @PageSize ROWS ONLY;

Il ROW_NUMBERpiano ha un costo stimato di 0,0197935 :

Piano dei numeri di riga

Il OFFSETpiano ha un costo stimato di 0,0196955 :

Piano di compensazione

Si tratta di un risparmio di 0,000098 unità di costo stimato (sebbene il OFFSETpiano richiederebbe operatori extra se si desidera restituire un numero di riga per ogni riga). Il OFFSETpiano sarà ancora leggermente più economico, in generale, ma ricorda che i costi stimati sono esattamente questo: sono ancora necessari test reali. La maggior parte del costo in entrambi i piani è il costo dell'intero tipo di set di input, quindi indici utili andrebbero a beneficio di entrambe le soluzioni.

Laddove vengono utilizzati valori letterali costanti (ad esempio OFFSET 30nell'esempio originale), l'ottimizzatore può utilizzare un ordinamento TopN anziché un ordinamento completo seguito da un valore superiore. Quando le righe necessarie per l'ordinamento TopN sono letterali costanti e <= 100 (la somma di OFFSETe FETCH) il motore di esecuzione può utilizzare un algoritmo di ordinamento diverso che può eseguire più velocemente dell'ordinamento TopN generalizzato. Tutti e tre i casi hanno caratteristiche prestazionali diverse nel complesso.

Per quanto riguarda il motivo per cui l'ottimizzatore non trasforma automaticamente il ROW_NUMBERmodello di sintassi da utilizzare OFFSET, ci sono una serie di motivi:

  1. È quasi impossibile scrivere una trasformazione che corrisponda a tutti gli usi esistenti
  2. La trasformazione automatica di alcune query di paging e non altre potrebbe creare confusione
  3. Il OFFSETpiano non è garantito per essere migliore in tutti i casi

Un esempio per il terzo punto precedente si verifica in cui il set di paging è piuttosto ampio. Può essere molto più efficiente cercare le chiavi necessarie utilizzando un indice non cluster e cercare manualmente l'indice cluster rispetto alla scansione dell'indice con OFFSETo ROW_NUMBER. Vi sono ulteriori problemi da considerare se l'applicazione di paging deve sapere quante righe o pagine sono presenti in totale. C'è un'altra buona discussione sui meriti relativi dei metodi "ricerca chiave" e "offset" qui .

Nel complesso, è probabilmente meglio che le persone prendano una decisione informata di modificare le loro query di paging da utilizzare OFFSET, se appropriato, dopo test approfonditi.


1
Quindi la ragione per cui la trasformazione non viene eseguita nei casi comuni è probabilmente che era troppo difficile trovare un compromesso ingegneristico accettabile. Hai fornito buoni motivi per cui questo potrebbe essere stato il caso .; Devo dire che questa è una buona risposta. Molte intuizioni e nuovi pensieri. Lascio la domanda aperta per un po 'e quindi scelgo la risposta migliore.
usr

5

Con una leggera manipolazione della tua query ottengo una stima del costo uguale (50/50) e statistiche IO uguali:

; WITH cte AS
(
    SELECT *, ROW_NUMBER() OVER (ORDER BY object_id) r
    FROM #objects
)
SELECT *
FROM cte
WHERE r >= 30 AND r < 40
ORDER BY r

SELECT *
FROM #objects
ORDER BY object_id
OFFSET 30 ROWS FETCH NEXT 10 ROWS ONLY

Questo evita l'ordinamento aggiuntivo che appare nella tua versione ordinando rinvece che object_id.


Grazie per questa intuizione. Ora che ci penso, ho visto l'ottimizzatore non capire prima la natura ordinata dell'output ROW_NUMBER. Considera l'insieme non ordinato da object_id. O almeno non ordinati sia per re object_id.
usr

2
@usr l'ORDINE BY utilizzato da ROW_NUMBER () definisce il modo in cui assegna i numeri. Non fa nulla per promettere l'ordine di uscita: è separato. Accade solo che spesso coincida, ma non è garantito.
Aaron Bertrand

@AaronBertrand Comprendo che ROW_NUMBER non ordina l'output. Ma se ROW_NUMBER è ordinato dalle stesse colonne dell'output, lo stesso ordine è garantito, giusto? Quindi Query Optimizer potrebbe sfruttare questo fatto. Pertanto, in questa query non sono sempre necessarie due operazioni di ordinamento .
usr

1
@usr hai riscontrato un caso d'uso comune per cui l'ottimizzatore non tiene conto, ma non è l' unico caso d'uso. Considera i casi in cui l'ordine all'interno di ROW_NUMBER () è quella colonna e qualcos'altro. O quando l'ordine esterno esegue l'ordinamento secondario su un'altra colonna. O quando vuoi ordinare in ordine decrescente. O da qualcos'altro del tutto. Mi piace ordinare in base all'espressione ranziché nella colonna di base, se non altro perché corrisponde a ciò che farei in una query non nidificata e ordinare in base a un'espressione: utilizzerei l'alias assegnato all'espressione invece di ripetere l'espressione.
Aaron Bertrand

4
@usr E al punto di Paul, ci saranno casi in cui puoi trovare lacune nella funzionalità nell'ottimizzatore. Se non verranno corretti e conosci un modo migliore per scrivere la query, usa il modo migliore. Paziente: "Dottore, fa male quando faccio x." Dottore: "Non fare x". :-)
Aaron Bertrand

-3

Hanno modificato lo Strumento per ottimizzare le query per aggiungere questa funzione. Ciò significa che hanno implementato meccanismi specifici per supportare il comando offset ... fetch. In altre parole per la query principale, SQL Server deve svolgere molto più lavoro. Quindi la differenza nei piani di query.

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.