Problema di ottimizzazione con la funzione definita dall'utente


26

Ho un problema nel capire perché SQL Server decide di chiamare la funzione definita dall'utente per ogni valore nella tabella anche se è necessario recuperare solo una riga. L'attuale SQL è molto più complesso, ma sono stato in grado di ridurre il problema fino a questo:

select  
    S.GROUPCODE,
    H.ORDERCATEGORY
from    
    ORDERLINE L
    join ORDERHDR H on H.ORDERID = L.ORDERID
    join PRODUCT P  on P.PRODUCT = L.PRODUCT    
    cross apply dbo.GetGroupCode (P.FACTORY) S
where   
    L.ORDERNUMBER = 'XXX/YYY-123456' and
    L.RMPHASE = '0' and
    L.ORDERLINE = '01'

Per questa query, SQL Server decide di chiamare la funzione GetGroupCode per ogni singolo valore esistente nella tabella PRODOTTO, anche se la stima e il numero effettivo di righe restituite da ORDERLINE è 1 (è la chiave primaria):

Piano di query

Lo stesso piano in Esplora piani che mostra i conteggi delle righe:

Plan explorer tabelle:

ORDERLINE: 1.5M rows, primary key: ORDERNUMBER + ORDERLINE + RMPHASE (clustered)
ORDERHDR:  900k rows, primary key: ORDERID (clustered)
PRODUCT:   6655 rows, primary key: PRODUCT (clustered)

L'indice utilizzato per la scansione è:

create unique nonclustered index PRODUCT_FACTORY on PRODUCT (PRODUCT, FACTORY)

La funzione è in realtà leggermente più complessa, ma la stessa cosa accade con una fittizia funzione multiistruzione come questa:

create function GetGroupCode (@FACTORY varchar(4))
returns @t table(
    TYPE        varchar(8),
    GROUPCODE   varchar(30)
)
as begin
    insert into @t (TYPE, GROUPCODE) values ('XX', 'YY')
    return
end

Sono stato in grado di "correggere" le prestazioni forzando il server SQL a recuperare il primo prodotto 1, sebbene 1 sia il massimo che sia mai stato trovato:

select  
    S.GROUPCODE,
    H.ORDERCAT
from    
    ORDERLINE L
    join ORDERHDR H
        on H.ORDERID = M.ORDERID
    cross apply (select top 1 P.FACTORY from PRODUCT P where P.PRODUCT = L.PRODUCT) P
    cross apply dbo.GetGroupCode (P.FACTORY) S
where   
    L.ORDERNUMBER = 'XXX/YYY-123456' and
    L.RMPHASE = '0' and
    L.ORDERLINE = '01'

Quindi anche la forma del piano cambia in qualcosa che mi aspettavo che fosse originariamente:

Piano di query con inizio

Ho anche pensato che l'indice PRODUCT_FACTORY essendo più piccolo dell'indice cluster PRODUCT_PK avrebbe un effetto, ma anche costringendo la query a utilizzare PRODUCT_PK, il piano è sempre lo stesso dell'originale, con 6655 chiamate alla funzione.

Se tralascio completamente ORDERHDR, il piano inizia prima con un ciclo nidificato tra ORDERLINE e PRODUCT e la funzione viene chiamata una sola volta.

Vorrei capire quale potrebbe essere la ragione di ciò poiché tutte le operazioni vengono eseguite utilizzando le chiavi primarie e come risolverlo se si verifica in una query più complessa che non può essere risolta così facilmente.

Modifica: crea istruzioni per la tabella:

CREATE TABLE dbo.ORDERHDR(
    ORDERID varchar(8) NOT NULL,
    ORDERCATEGORY varchar(2) NULL,
    CONSTRAINT ORDERHDR_PK PRIMARY KEY CLUSTERED (ORDERID)
)

CREATE TABLE dbo.ORDERLINE(
    ORDERNUMBER varchar(16) NOT NULL,
    RMPHASE char(1) NOT NULL,
    ORDERLINE char(2) NOT NULL,
    ORDERID varchar(8) NOT NULL,
    PRODUCT varchar(8) NOT NULL,
    CONSTRAINT ORDERLINE_PK PRIMARY KEY CLUSTERED (ORDERNUMBER,ORDERLINE,RMPHASE)
)

CREATE TABLE dbo.PRODUCT(
    PRODUCT varchar(8) NOT NULL,
    FACTORY varchar(4) NULL,
    CONSTRAINT PRODUCT_PK PRIMARY KEY CLUSTERED (PRODUCT)
)

Risposte:


30

Ci sono tre principali ragioni tecniche per ottenere il piano che fai:

  1. Il framework dei costi dell'ottimizzatore non ha un reale supporto per le funzioni non in linea. Non fa alcun tentativo di guardare all'interno della definizione della funzione per vedere quanto potrebbe essere costoso, assegna solo un costo fisso molto piccolo e stima che la funzione produrrà 1 riga di output ogni volta che viene chiamata. Entrambe queste ipotesi di modellazione sono molto spesso completamente non sicure. La situazione è leggermente migliorata nel 2014 con il nuovo stimatore della cardinalità abilitato poiché l'ipotesi a 1 riga fissa viene sostituita con un'ipotesi a 100 righe fissa. Tuttavia, non esiste ancora alcun supporto per il costo del contenuto delle funzioni non in linea.
  2. SQL Server inizialmente comprime i join e si applica in un singolo join logico interno n-ary. Questo aiuta il motivo dell'ottimizzatore per unire gli ordini in seguito. L'espansione del single n-ary join negli ordini dei candidati candidati arriva più tardi e si basa in gran parte sull'euristica. Ad esempio, i join interni vengono prima dei join esterni, delle tabelle piccole e dei join selettivi prima delle tabelle di grandi dimensioni e dei join meno selettivi e così via.
  3. Quando SQL Server esegue l'ottimizzazione basata sui costi, suddivide lo sforzo in fasi opzionali per ridurre al minimo le possibilità di spendere troppo tempo ottimizzando le query a basso costo. Esistono tre fasi principali, la ricerca 0, la ricerca 1 e la ricerca 2. Ogni fase ha condizioni di accesso e le fasi successive consentono esplorazioni più ottimistiche rispetto a quelle precedenti. La tua query sembra qualificarsi per la fase di ricerca meno capace, fase 0. Qui viene trovato un piano di costi abbastanza basso da non inserire fasi successive.

Data la stima di cardinalità ridotta assegnata all'UDF, l'euristica di espansione del join n-ary purtroppo la riposiziona prima nella struttura di quanto si desideri.

La query si qualifica anche per l'ottimizzazione della ricerca 0 in virtù della presenza di almeno tre join (incluso si applica). Il piano fisico finale che ottieni, con la scansione dall'aspetto strano, si basa su quell'ordine di join dedotto euristicamente. Il costo è abbastanza basso che l'ottimizzatore considera il piano "abbastanza buono". La stima a basso costo e la cardinalità per l'UDF contribuiscono a questo primo traguardo.

La ricerca 0 (nota anche come fase di elaborazione delle transazioni) si rivolge a query di tipo OLTP a bassa cardinalità, con piani finali che di solito prevedono join di cicli nidificati. Ancora più importante, la ricerca 0 esegue solo un sottoinsieme relativamente piccolo delle capacità di esplorazione dell'ottimizzatore. Questo sottoinsieme non include il pull di un applicare l'albero delle query su un join (regola PullApplyOverJoin). Questo è esattamente ciò che è richiesto nel caso di test per riposizionare l'UDF applicato sopra i join, per apparire per ultimo nella sequenza delle operazioni (per così dire).

Esiste anche un problema in cui l'ottimizzatore può decidere tra join loop nidificati ingenui (predicato join sull'unione stessa) e un join indicizzato correlato (applica) in cui il predicato correlato viene applicato sul lato interno del join utilizzando una ricerca indice. Quest'ultima è in genere la forma desiderata del piano, ma l'ottimizzatore è in grado di esplorare entrambi. Con stime errate di costi e cardinalità, può scegliere il join NL non applicabile, come nei piani inviati (che spiega la scansione).

Quindi, ci sono molteplici motivi di interazione che coinvolgono diverse funzioni di ottimizzazione generale che normalmente funzionano bene per trovare buoni piani in un breve periodo di tempo senza utilizzare risorse eccessive. Evitare uno dei motivi è sufficiente per produrre la forma del piano "prevista" per la query di esempio, anche con tabelle vuote:

Pianifica su tabelle vuote con la ricerca 0 disabilitata

Non esiste un modo supportato per evitare la selezione del piano di ricerca 0, la chiusura anticipata dell'ottimizzatore o per migliorare il costo degli UDF (a parte i limitati miglioramenti nel modello CE di SQL Server 2014 per questo). Questo lascia cose come guide di piano, riscritture manuali di query (compresa l' TOP (1)idea o l'utilizzo di tabelle temporanee intermedie) ed evitando "scatole nere" a basso costo (dal punto di vista del QO) come funzioni non in linea.

Riscrivere CROSS APPLYcome OUTER APPLYpuò anche funzionare, poiché attualmente impedisce alcune delle prime operazioni di collasso dei join, ma è necessario fare attenzione a preservare la semantica della query originale (ad esempio, rifiutando eventuali NULLrighe estese che potrebbero essere introdotte, senza che l'ottimizzatore ritorni a croce si applicano). È necessario essere consapevoli del fatto che non è garantito che questo comportamento rimanga stabile, pertanto è necessario ricordare di ripetere il test di tali comportamenti osservati ogni volta che si corregge o si aggiorna SQL Server.

Nel complesso, la soluzione giusta per te dipende da una varietà di fattori che non possiamo giudicare per te. Ti incoraggio, tuttavia, a prendere in considerazione soluzioni che sono garantite per funzionare sempre in futuro e che lavorano con (anziché contro) l'ottimizzatore, ove possibile.


24

Sembra che questa sia una decisione basata sui costi dell'ottimizzatore, ma piuttosto negativa.

Se aggiungi 50000 righe a PRODUCT, l'ottimizzatore ritiene che la scansione sia troppo impegnativa e ti fornisce un piano con tre ricerche e una chiamata all'UDF.

Il piano che ottengo per 6655 righe in PRODOTTO

inserisci qui la descrizione dell'immagine

Con 50000 righe in PRODOTTO ottengo invece questo piano.

inserisci qui la descrizione dell'immagine

Immagino che il costo per chiamare l'UDF sia gravemente sottovalutato.

Una soluzione alternativa che funziona correttamente in questo caso è quella di modificare la query in modo da utilizzare l'applicazione esterna sull'UDF. Ottengo il buon piano indipendentemente da quante righe ci sono nella tabella PRODOTTO.

select  
    S.GROUPCODE,
    H.ORDERCATEGORY
from    
    ORDERLINE L
    join ORDERHDR H on H.ORDERID = L.ORDERID
    join PRODUCT P  on P.PRODUCT = L.PRODUCT    
    outer apply dbo.GetGroupCode (P.FACTORY) S
where   
    L.ORDERNUMBER = 'XXX/YYY-123456' and
    L.RMPHASE = '0' and
    L.ORDERLINE = '01' and
    S.GROUPCODE is not null

inserisci qui la descrizione dell'immagine

La soluzione migliore nel tuo caso è probabilmente quella di ottenere i valori necessari in una tabella temporanea e quindi eseguire una query sulla tabella temporanea con una croce applicata all'UDF. In questo modo sei sicuro che l'UDF non verrà eseguito più del necessario.

select  
    P.FACTORY,
    H.ORDERCATEGORY
into #T
from    
    ORDERLINE L
    join ORDERHDR H on H.ORDERID = L.ORDERID
    join PRODUCT P  on P.PRODUCT = L.PRODUCT
where   
    L.ORDERNUMBER = 'XXX/YYY-123456' and
    L.RMPHASE = '0' and
    L.ORDERLINE = '01'

select  
    S.GROUPCODE,
    T.ORDERCATEGORY
from #T as T
  cross apply dbo.GetGroupCode (T.FACTORY) S

drop table #T

Invece di persistere nella tabella temporanea, è possibile utilizzare top()in una tabella derivata per forzare SQL Server a valutare il risultato dai join prima che venga chiamato l'UDF. Basta usare un numero molto alto nella parte superiore di SQL Server per contare le righe per quella parte della query prima che possa continuare e utilizzare l'UDF.

select S.GROUPCODE,
       T.ORDERCATEGORY
from (
     select top(2147483647)
         P.FACTORY,
         H.ORDERCATEGORY
     from    
         ORDERLINE L
         join ORDERHDR H on H.ORDERID = L.ORDERID
         join PRODUCT P  on P.PRODUCT = L.PRODUCT    
     where   
         L.ORDERNUMBER = 'XXX/YYY-123456' and
         L.RMPHASE = '0' and
         L.ORDERLINE = '01'
     ) as T
  cross apply dbo.GetGroupCode (T.FACTORY) S

inserisci qui la descrizione dell'immagine

Vorrei capire quale potrebbe essere la ragione di ciò poiché tutte le operazioni vengono eseguite utilizzando le chiavi primarie e come risolverlo se si verifica in una query più complessa che non può essere risolta così facilmente.

Non posso proprio rispondere, ma ho pensato di condividere comunque quello che so. Non so perché venga presa in considerazione una scansione della tabella PRODUCT. Potrebbero esserci casi in cui questa è la cosa migliore da fare e ci sono cose su come gli ottimizzatori trattano gli UDF di cui non sono a conoscenza.

Un'ulteriore osservazione è stata che la tua query ottiene un buon piano in SQL Server 2014 con il nuovo strumento di stima della cardinalità. Questo perché il numero stimato di righe per ogni chiamata all'UDF è 100 anziché 1 come in SQL Server 2012 e precedenti. Tuttavia, prenderà comunque la stessa decisione basata sui costi tra la versione di scansione e la versione di ricerca del piano. Con meno di 500 (497 nel mio caso) righe in PRODUCT ottieni la versione di scansione del piano anche in SQL Server 2014.


2
In qualche modo mi ricorda la sessione di Adam Machanic a SQL Bits: sqlbits.com/Sessions/Event14/…
James Z,
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.