Database
 sql >> Database >  >> RDS >> Database

UNION ALL Ottimizzazione

La concatenazione di due o più set di dati è più comunemente espressa in T-SQL usando UNION ALL clausola. Dato che l'ottimizzatore di SQL Server può spesso riordinare elementi come join e aggregati per migliorare le prestazioni, è abbastanza ragionevole aspettarsi che SQL Server consideri anche il riordino degli input di concatenazione, laddove ciò rappresenterebbe un vantaggio. Ad esempio, l'ottimizzatore potrebbe considerare i vantaggi della riscrittura di A UNION ALL B come B UNION ALL A .

In effetti, l'ottimizzatore di SQL Server non Fai questo. Più precisamente, c'era un supporto limitato per il riordino dell'input di concatenazione nelle versioni di SQL Server fino al 2008 R2, ma questo è stato rimosso in SQL Server 2012 e da allora non è più riemerso.

SQL Server 2008 R2

Intuitivamente, l'ordine degli input di concatenazione conta solo se è presente un obiettivo di riga . Per impostazione predefinita, SQL Server ottimizza i piani di esecuzione in base al fatto che tutte le righe qualificanti verranno restituite al client. Quando è attivo un obiettivo di riga, l'ottimizzatore cerca di trovare un piano di esecuzione che produca rapidamente le prime righe.

Gli obiettivi di riga possono essere impostati in diversi modi, ad esempio utilizzando TOP , un FAST n suggerimento per la query o utilizzando EXISTS (che per sua natura deve trovare al massimo una riga). Laddove non ci sia un obiettivo di riga (ovvero il client richiede tutte le righe), generalmente non importa in quale ordine vengono letti gli input di concatenazione:ogni input verrà comunque elaborato in ogni caso.

Il supporto limitato nelle versioni fino a SQL Server 2008 R2 si applica laddove esiste l'obiettivo di esattamente una riga . In questa specifica circostanza, SQL Server riordinerà gli input di concatenazione in base al costo previsto.

Questo non viene fatto durante l'ottimizzazione basata sui costi (come ci si potrebbe aspettare), ma piuttosto come una riscrittura post-ottimizzazione dell'ultimo minuto del normale output dell'ottimizzatore. Questa disposizione ha il vantaggio di non aumentare lo spazio di ricerca del piano basato sui costi (potenzialmente un'alternativa per ogni possibile riordino), producendo comunque un piano ottimizzato per restituire rapidamente la prima riga.

Esempi

Gli esempi seguenti utilizzano due tabelle con contenuto identico:Un milione di righe di numeri interi da uno a un milione. Una tabella è un heap senza indici non cluster; l'altro ha un indice cluster univoco:

CREATE TABLE dbo.Expensive
(
    Val bigint NOT NULL
);
 
CREATE TABLE dbo.Cheap
(
    Val bigint NOT NULL, 
 
    CONSTRAINT [PK dbo.Cheap Val]
        UNIQUE CLUSTERED (Val)
);
GO
INSERT dbo.Cheap WITH (TABLOCKX)
    (Val)
SELECT TOP (1000000)
    Val = ROW_NUMBER() OVER (ORDER BY SV1.number)
FROM master.dbo.spt_values AS SV1
CROSS JOIN master.dbo.spt_values AS SV2
ORDER BY
    Val
OPTION (MAXDOP 1);
GO
INSERT dbo.Expensive WITH (TABLOCKX)
    (Val)
SELECT
    C.Val
FROM dbo.Cheap AS C
OPTION (MAXDOP 1);

Nessun obiettivo di fila

La query seguente cerca le stesse righe in ogni tabella e restituisce la concatenazione dei due insiemi:

SELECT E.Val 
FROM dbo.Expensive AS E 
WHERE 
    E.Val BETWEEN 751000 AND 751005
 
UNION ALL
 
SELECT C.Val
FROM dbo.Cheap AS C 
WHERE 
    C.Val BETWEEN 751000 AND 751005;

Il piano di esecuzione prodotto da Query Optimizer è:

L'avviso sulla radice SELECT l'operatore ci sta avvisando dell'evidente indice mancante nella tabella heap. L'avviso sull'operatore Table Scan viene aggiunto da Sentry One Plan Explorer. Sta attirando la nostra attenzione sul costo di I/O del predicato residuo nascosto nella scansione.

L'ordine degli input per la concatenazione non ha importanza qui, perché non abbiamo fissato un obiettivo di fila. Entrambi gli input verranno letti completamente per restituire tutte le righe dei risultati. Di interesse (sebbene ciò non sia garantito) si noti che l'ordine degli input segue l'ordine testuale della query originale. Si noti inoltre che non è specificato nemmeno l'ordine delle righe del risultato finale, poiché non è stato utilizzato un ORDER BY di livello superiore clausola. Daremo per scontato che sia deliberato e che l'ordine finale sia irrilevante per il compito da svolgere.

Se invertiamo l'ordine di scrittura delle tabelle nella query in questo modo:

SELECT C.Val
FROM dbo.Cheap AS C 
WHERE 
    C.Val BETWEEN 751000 AND 751005
 
UNION ALL
 
SELECT E.Val 
FROM dbo.Expensive AS E 
WHERE 
    E.Val BETWEEN 751000 AND 751005;

Il piano di esecuzione segue la modifica, accedendo prima alla tabella cluster (di nuovo, questo non è garantito):

Ci si può aspettare che entrambe le query abbiano le stesse caratteristiche di prestazione, poiché eseguono le stesse operazioni, solo in un ordine diverso.

Con un obiettivo di fila

Chiaramente, la mancanza di indicizzazione sulla tabella heap renderà normalmente più costosa la ricerca di righe specifiche, rispetto alla stessa operazione sulla tabella cluster. Se chiediamo all'ottimizzatore un piano che restituisca rapidamente la prima riga, ci aspetteremmo che SQL Server riordini gli input di concatenazione in modo che venga prima consultata la tabella cluster economica.

Utilizzando la query che menziona prima la tabella heap e utilizzando un suggerimento per la query FAST 1 per specificare l'obiettivo della riga:

SELECT E.Val 
FROM dbo.Expensive AS E 
WHERE 
    E.Val BETWEEN 751000 AND 751005
 
UNION ALL
 
SELECT C.Val
FROM dbo.Cheap AS C 
WHERE 
    C.Val BETWEEN 751000 AND 751005
OPTION (FAST 1);

Il piano di esecuzione stimato prodotto su un'istanza di SQL Server 2008 R2 è:

Si noti che gli input di concatenazione sono stati riordinati per ridurre il costo stimato per la restituzione della prima riga. Si noti inoltre che l'indice mancante e gli avvisi di I/O residui sono scomparsi. Nessuno dei due problemi è rilevante con questa forma del piano quando l'obiettivo è restituire una singola riga il più rapidamente possibile.

La stessa query eseguita su SQL Server 2016 (utilizzando uno dei modelli di stima della cardinalità) è:

SQL Server 2016 non ha riordinato gli input di concatenazione. L'avviso di I/O di Plan Explorer è tornato, ma purtroppo l'ottimizzatore non ha prodotto un avviso di indice mancante questa volta (sebbene sia rilevante).

Riordino generale

Come accennato, la riscrittura post-ottimizzazione che riordina gli input di concatenazione è efficace solo per:

  • SQL Server 2008 R2 e versioni precedenti
  • Una riga di esattamente uno

Se vogliamo davvero che venga restituita solo una riga, anziché un piano ottimizzato per restituire rapidamente la prima riga (ma che alla fine restituirà comunque tutte le righe), possiamo utilizzare un TOP clausola con una tabella derivata o un'espressione di tabella comune (CTE):

SELECT TOP (1)
    UA.Val
FROM
(
    SELECT E.Val 
    FROM dbo.Expensive AS E 
    WHERE 
        E.Val BETWEEN 751000 AND 751005
 
    UNION ALL
 
    SELECT C.Val
    FROM dbo.Cheap AS C 
    WHERE 
        C.Val BETWEEN 751000 AND 751005
) AS UA;

In SQL Server 2008 R2 o versioni precedenti, questo produce il piano di input riordinato ottimale:

In SQL Server 2012, 2014 e 2016 non si verifica alcun riordino successivo all'ottimizzazione:

Se vogliamo che venga restituita più di una riga, ad esempio utilizzando TOP (2) , la riscrittura desiderata non verrà applicata su SQL Server 2008 R2 anche se FAST 1 viene utilizzato anche il suggerimento. In quella situazione, dobbiamo ricorrere a trucchi come usare TOP con una variabile e un OPTIMIZE FOR suggerimento:

DECLARE @TopRows bigint = 2; -- Number of rows actually needed
 
SELECT TOP (@TopRows)
    UA.Val
FROM
(
    SELECT E.Val 
    FROM dbo.Expensive AS E 
    WHERE 
        E.Val BETWEEN 751000 AND 751005
 
    UNION ALL
 
    SELECT C.Val
    FROM dbo.Cheap AS C 
    WHERE 
        C.Val BETWEEN 751000 AND 751005
) AS UA
OPTION (OPTIMIZE FOR (@TopRows = 1)); -- Just a hint

L'hint della query è sufficiente per impostare un obiettivo di riga pari a uno, mentre il valore di runtime della variabile garantisce che venga restituito il numero desiderato di righe (2).

Il piano di esecuzione effettivo su SQL Server 2008 R2 è:

Entrambe le righe restituite provengono dall'input di ricerca riordinato e la scansione della tabella non viene eseguita affatto. Plan Explorer mostra i conteggi delle righe in rosso perché la stima riguardava una riga (a causa del suggerimento) mentre sono state rilevate due righe in fase di esecuzione.

Senza UNION ALL

Anche questo problema non è limitato alle query scritte in modo esplicito con UNION ALL . Altre costruzioni come EXISTS e OR può anche comportare che l'ottimizzatore introduca un operatore di concatenazione, che potrebbe risentire della mancanza di riordino dell'input. C'è stata una domanda recente su Database Administrators Stack Exchange con esattamente questo problema. Trasformare la query da quella domanda per utilizzare le nostre tabelle di esempio:

SELECT
    CASE
        WHEN 
            EXISTS 
            (
                SELECT 1
                FROM dbo.Expensive AS E
                WHERE E.Val BETWEEN 751000 AND 751005
            ) 
            OR EXISTS 
            (
                SELECT 1
                FROM dbo.Cheap AS C 
                WHERE C.Val BETWEEN 751000 AND 751005
            ) 
            THEN 1
        ELSE 0
    END;

Il piano di esecuzione su SQL Server 2016 ha la tabella heap nel primo input:

In SQL Server 2008 R2 l'ordine degli input è ottimizzato per riflettere l'obiettivo di riga singola del semi join:

Nel piano più ottimale, la scansione dell'heap non viene mai eseguita.

Soluzioni alternative

In alcuni casi, sarà evidente all'autore della query che uno degli input di concatenazione sarà sempre più economico da eseguire rispetto agli altri. Se ciò è vero, è abbastanza valido riscrivere la query in modo che gli input di concatenazione più economici appaiano per primi in ordine scritto. Ovviamente ciò significa che lo scrittore di query deve essere consapevole di questa limitazione dell'ottimizzatore e pronto a fare affidamento su comportamenti non documentati.

Un problema più difficile sorge quando il costo degli input di concatenazione varia a seconda delle circostanze, forse a seconda dei valori dei parametri. Usando OPTION (RECOMPILE) non sarà di aiuto su SQL Server 2012 o versioni successive. Questa opzione può essere utile su SQL Server 2008 R2 o versioni precedenti, ma solo se viene soddisfatto anche il requisito dell'obiettivo di una riga.

Se ci sono dubbi sull'affidarsi al comportamento osservato (input di concatenazione del piano di query che corrispondono all'ordine del testo della query) è possibile utilizzare una guida del piano per forzare la forma del piano. Laddove ordini di input diversi siano ottimali per circostanze diverse, è possibile utilizzare più guide del piano, in cui le condizioni possono essere codificate accuratamente in anticipo. Tuttavia, non è certo l'ideale.

Pensieri finali

Query Optimizer di SQL Server contiene infatti un basato sui costi regola di esplorazione, UNIAReorderInputs , che è in grado di generare variazioni dell'ordine di input di concatenazione ed esplorare alternative durante l'ottimizzazione basata sui costi (non come una riscrittura post-ottimizzazione singola).

Questa regola non è attualmente abilitata per l'uso generale. Per quanto ne so, viene attivato solo quando una guida al piano o USE PLAN suggerimento è presente. Ciò consente al motore di forzare correttamente un piano che è stato generato per una query che si è qualificata per la riscrittura del riordino dell'input, anche quando la query corrente non è idonea.

La mia sensazione è che questa regola di esplorazione sia deliberatamente limitata a questo uso, perché le query che trarrebbero vantaggio dal riordino dell'input di concatenazione come parte dell'ottimizzazione basata sui costi non sono considerate sufficientemente comuni, o forse perché c'è la preoccupazione che lo sforzo extra non ripagherebbe spento. La mia opinione è che il riordino dell'input dell'operatore di concatenazione dovrebbe sempre essere esplorato quando è attivo un obiettivo di riga.

È anche un peccato che la riscrittura post-ottimizzazione (più limitata) non sia efficace in SQL Server 2012 o versioni successive. Ciò potrebbe essere dovuto a un bug sottile, ma non sono riuscito a trovare nulla al riguardo nella documentazione, nella knowledge base o su Connect. Ho aggiunto un nuovo elemento Connect qui.

Aggiornamento del 9 agosto 2017 :Ora è risolto sotto il flag di traccia 4199 per SQL Server 2014 e 2016, vedere KB 4023419:

FIX:la query con UNION ALL e un obiettivo di riga potrebbe essere più lenta in SQL Server 2014 o versioni successive rispetto a SQL Server 2008 R2