ordine delle clausole in "EXISTS (...) OR EXISTS (...)"


11

Ho una classe di domande che verificano l'esistenza di una di queste due cose. È della forma

SELECT CASE
  WHEN EXISTS (SELECT 1 FROM ...)
  OR EXISTS (SELECT 1 FROM ...)
THEN 1 ELSE 0 END;

L'istruzione effettiva viene generata in C ed eseguita come una query ad hoc su una connessione ODBC.

Recentemente è emerso che il secondo SELECT sarà probabilmente più veloce del primo SELECT nella maggior parte dei casi e che il cambio dell'ordine delle due clausole EXISTS ha causato una drastica accelerazione in almeno un caso di test abusivo che abbiamo appena creato.

La cosa ovvia da fare è solo andare avanti e cambiare le due clausole, ma volevo vedere se qualcuno che avesse più familiarità con SQL Server si sarebbe preoccupato di tenerne conto. Mi sembra di fare affidamento su una coincidenza e un "dettaglio di implementazione".

(Sembra anche che se SQL Server fosse più intelligente, eseguirà entrambe le clausole EXISTS in parallelo e consentirebbe a qualunque di esse di completare prima il primo cortocircuito dell'altra).

Esiste un modo migliore per consentire a SQL Server di migliorare costantemente il tempo di esecuzione di tale query?

Aggiornare

Grazie per il tuo tempo e interesse per la mia domanda. Non mi aspettavo domande sugli effettivi piani di query, ma sono disposto a condividerli.

Questo è per un componente software che supporta SQL Server 2008R2 e versioni successive. La forma dei dati può essere abbastanza diversa a seconda della configurazione e dell'uso. Il mio collega ha pensato di apportare questa modifica alla query perché la dbf_1162761$z$rv$1257927703tabella (nell'esempio) avrà sempre maggiore o uguale al numero di righe in essa rispetto alla dbf_1162761$z$dd$1257927703tabella - a volte significativamente più (ordini di grandezza).

Ecco il caso abusivo che ho citato. La prima query è quella lenta e richiede circa 20 secondi. La seconda query viene completata in un istante.

Per quello che vale, anche il bit "OTTIMIZZA PER SCONOSCIUTO" è stato aggiunto di recente perché lo sniffing dei parametri ha eliminato alcuni casi.

Query originale:

SELECT CASE
  WHEN EXISTS (SELECT 1 FROM zumero.dbf_1162761$z$rv$1257927703 rv INNER JOIN zumero.dbf_1162761$t$tx tx ON tx.txid=rv.txid WHERE tx.generation BETWEEN 1500 AND 2502)
  OR EXISTS (SELECT 1 FROM zumero.dbf_1162761$z$dd$1257927703 dd INNER JOIN zumero.dbf_1162761$t$tx tx ON tx.txid=dd.txid WHERE tx.generation BETWEEN 1500 AND 2502)
THEN 1 ELSE 0 END
OPTION (OPTIMIZE FOR UNKNOWN)

Piano originale:

|--Compute Scalar(DEFINE:([Expr1006]=CASE WHEN [Expr1007] THEN (1) ELSE (0) END))
     |--Nested Loops(Left Semi Join, DEFINE:([Expr1007] = [PROBE VALUE]))
          |--Constant Scan
          |--Concatenation
               |--Nested Loops(Inner Join, WHERE:([scale].[zumero].[dbf_1162761$z$rv$1257927703].[txid] as [rv].[txid]=[scale].[zumero].[dbf_1162761$t$tx].[txid] as [tx].[txid]))
               |    |--Clustered Index Scan(OBJECT:([scale].[zumero].[dbf_1162761$z$rv$1257927703].[PK__dbf_1162__97770A2F62EEAE79] AS [rv]), WHERE:([scale].[zumero].[dbf_1162761$z$rv$1257927703].[txid] as [rv].[txid]>(0)))
               |    |--Index Seek(OBJECT:([scale].[zumero].[dbf_1162761$t$tx].[gendex] AS [tx]), SEEK:([tx].[generation] >= (1500) AND [tx].[generation] <= (2502)) ORDERED FORWARD)
               |--Nested Loops(Inner Join, OUTER REFERENCES:([tx].[txid]))
                    |--Clustered Index Scan(OBJECT:([scale].[zumero].[dbf_1162761$t$tx].[PK__dbf_1162__E3BA953EC2197789] AS [tx]),  WHERE:([scale].[zumero].[dbf_1162761$t$tx].[generation] as [tx].[generation]>=(1500) AND [scale].[zumero].[dbf_1162761$t$tx].[generation] as [tx].[generation]<=(2502)) ORDERED FORWARD)
                    |--Index Seek(OBJECT:([scale].[zumero].[dbf_1162761$z$dd$1257927703].[n$dbf_1162761$z$dd$txid$1257927703] AS [dd]), SEEK:([dd].[txid]=[scale].[zumero].[dbf_1162761$t$tx].[txid] as [tx].[txid]),  WHERE:([scale].[zumero].[dbf_1162761$z$dd$1257927703].[txid] as [dd].[txid]>(0)) ORDERED FORWARD)

Query fissa:

SELECT CASE
  WHEN EXISTS (SELECT 1 FROM zumero.dbf_1162761$z$dd$1257927703 dd INNER JOIN zumero.dbf_1162761$t$tx tx ON tx.txid=dd.txid WHERE tx.generation BETWEEN 1500 AND 2502)
  OR EXISTS (SELECT 1 FROM zumero.dbf_1162761$z$rv$1257927703 rv INNER JOIN zumero.dbf_1162761$t$tx tx ON tx.txid=rv.txid WHERE tx.generation BETWEEN 1500 AND 2502)
THEN 1 ELSE 0 END
OPTION (OPTIMIZE FOR UNKNOWN)

Piano fisso:

|--Compute Scalar(DEFINE:([Expr1006]=CASE WHEN [Expr1007] THEN (1) ELSE (0) END))
     |--Nested Loops(Left Semi Join, DEFINE:([Expr1007] = [PROBE VALUE]))
          |--Constant Scan
          |--Concatenation
               |--Nested Loops(Inner Join, OUTER REFERENCES:([tx].[txid]))
               |    |--Clustered Index Scan(OBJECT:([scale].[zumero].[dbf_1162761$t$tx].[PK__dbf_1162__E3BA953EC2197789] AS [tx]),  WHERE:([scale].[zumero].[dbf_1162761$t$tx].[generation] as [tx].[generation]>=(1500) AND [scale].[zumero].[dbf_1162761$t$tx].[generation] as [tx].[generation]<=(2502)) ORDERED FORWARD)
               |    |--Index Seek(OBJECT:([scale].[zumero].[dbf_1162761$z$dd$1257927703].[n$dbf_1162761$z$dd$txid$1257927703] AS [dd]), SEEK:([dd].[txid]=[scale].[zumero].[dbf_1162761$t$tx].[txid] as [tx].[txid]),  WHERE:([scale].[zumero].[dbf_1162761$z$dd$1257927703].[txid] as [dd].[txid]>(0)) ORDERED FORWARD)
               |--Nested Loops(Inner Join, WHERE:([scale].[zumero].[dbf_1162761$z$rv$1257927703].[txid] as [rv].[txid]=[scale].[zumero].[dbf_1162761$t$tx].[txid] as [tx].[txid]))
                    |--Clustered Index Scan(OBJECT:([scale].[zumero].[dbf_1162761$z$rv$1257927703].[PK__dbf_1162__97770A2F62EEAE79] AS [rv]), WHERE:([scale].[zumero].[dbf_1162761$z$rv$1257927703].[txid] as [rv].[txid]>(0)))
                    |--Index Seek(OBJECT:([scale].[zumero].[dbf_1162761$t$tx].[gendex] AS [tx]), SEEK:([tx].[generation] >= (1500) AND [tx].[generation] <= (2502)) ORDERED FORWARD)

1
Domande e risposte
Paul White 9

Risposte:


11

Come regola generale, SQL Server eseguirà le parti di CASEun'istruzione in ordine ma è libero di riordinare le ORcondizioni. Per alcune query è possibile ottenere prestazioni costantemente migliori modificando l'ordine delle WHENespressioni all'interno di CASEun'istruzione. A volte è anche possibile ottenere prestazioni migliori quando si cambia l'ordine delle condizioni in ORun'istruzione, ma non è un comportamento garantito.

Probabilmente è meglio percorrerlo con un semplice esempio. Sto testando su SQL Server 2016, quindi è possibile che non otterrai gli stessi esatti risultati sulla tua macchina, ma per quanto ne so valgono gli stessi principi. Innanzitutto inserirò un milione di numeri interi da 1 a 1000000 in due tabelle, una con un indice cluster e una come heap:

CREATE TABLE dbo.X_HEAP (ID INT NOT NULL, FLUFF VARCHAR(100));

INSERT INTO dbo.X_HEAP  WITH (TABLOCK)
SELECT TOP (1000000) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)), REPLICATE('Z', 100)
FROM master..spt_values t1
CROSS JOIN master..spt_values t2
OPTION (MAXDOP 1);

CREATE TABLE dbo.X_CI (ID INT NOT NULL, FLUFF VARCHAR(100), PRIMARY KEY (ID));

INSERT INTO dbo.X_CI  WITH (TABLOCK)
SELECT TOP (1000000) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)), REPLICATE('Z', 100)
FROM master..spt_values t1
CROSS JOIN master..spt_values t2
OPTION (MAXDOP 1);

Considera la seguente query:

SELECT CASE
  WHEN EXISTS (SELECT 1 FROM dbo.X_HEAP WHERE ID = 500000)
  OR EXISTS (SELECT 1 FROM dbo.X_CI WHERE ID = 500000)
THEN 1 ELSE 0 END;

Sappiamo che la valutazione della sottoquery rispetto a quella X_CIsarà molto più economica rispetto alla sottoquery X_HEAP, soprattutto quando non esiste una riga corrispondente. Se non è presente una riga corrispondente, è sufficiente eseguire alcune letture logiche sulla tabella con un indice cluster. Tuttavia, dovremmo scansionare tutte le righe dell'heap per sapere che non esiste una riga corrispondente. Anche l'ottimizzatore lo sa. In linea di massima, l'utilizzo di un indice cluster per cercare una riga è molto economico rispetto alla scansione di una tabella.

Per questi dati di esempio scriverei la query in questo modo:

SELECT CASE
  WHEN EXISTS (SELECT 1 FROM dbo.X_CI WHERE ID = 500000) THEN 1 
  WHEN EXISTS (SELECT 1 FROM dbo.X_HEAP WHERE ID = 500000) THEN 1 
ELSE 0 END;

Ciò impone efficacemente a SQL Server di eseguire prima la subquery sulla tabella con un indice cluster. Ecco i risultati di SET STATISTICS IO, TIME ON:

Tabella 'X_CI'. Conteggio scansione 0, letture logiche 3, letture fisiche 0

Tempi di esecuzione di SQL Server: tempo CPU = 0 ms, tempo trascorso = 0 ms.

Esaminando il piano di query, se la ricerca dell'etichetta 1 restituisce dati diversi dalla scansione dell'etichetta 2 non è richiesto e non accadrà:

buona domanda

La seguente query è molto meno efficiente:

SELECT CASE
  WHEN EXISTS (SELECT 1 FROM dbo.X_HEAP WHERE ID = 500000) THEN 1 
  WHEN EXISTS (SELECT 1 FROM dbo.X_CI WHERE ID = 500000) THEN 1 
ELSE 0 END
OPTION (MAXDOP 1);

Guardando il piano di query, vediamo che la scansione sull'etichetta 2 avviene sempre. Se viene trovata una riga, la ricerca dell'etichetta 1 viene ignorata. Questo non è l'ordine che volevamo:

piano di query errato

Le prestazioni lo confermano:

Tabella 'X_HEAP'. Conteggio scansione 1, letture logiche 7247

Tempi di esecuzione di SQL Server: tempo CPU = 15 ms, tempo trascorso = 22 ms.

Tornando alla query originale, per questa query vedo la ricerca e la scansione valutate nell'ordine che è buono per le prestazioni:

SELECT CASE
  WHEN EXISTS (SELECT 1 FROM dbo.X_HEAP WHERE ID = 500000)
  OR EXISTS (SELECT 1 FROM dbo.X_CI WHERE ID = 500000)
THEN 1 ELSE 0 END;

E in questa query vengono valutati nell'ordine opposto:

SELECT CASE
  WHEN EXISTS (SELECT 1 FROM dbo.X_CI WHERE ID = 500000)
  OR EXISTS (SELECT 1 FROM dbo.X_HEAP WHERE ID = 500000)
THEN 1 ELSE 0 END;

Tuttavia, a differenza della precedente coppia di query, non c'è nulla che costringa l'ottimizzatore di query di SQL Server a valutare l'uno prima dell'altro. Non dovresti fare affidamento su quel comportamento per qualcosa di importante.

In conclusione, se è necessario valutare una sottoquery prima dell'altra, utilizzare CASEun'istruzione o un altro metodo per forzare l'ordinamento. Altrimenti sentiti libero di ordinare le subquery in una ORcondizione come preferisci, ma sappi che non c'è garanzia che l'ottimizzatore le eseguirà nell'ordine come scritto.

Addendum:

Una domanda di follow-up naturale è cosa si può fare se si desidera che SQL Server decida quale query è più economica ed esegua prima quella? Tutti i metodi finora sembrano essere implementati da SQL Server nell'ordine in cui la query è scritta, anche se per alcuni di essi non è garantito il comportamento.

Ecco un'opzione che sembra funzionare per le semplici tabelle demo:

SELECT CASE
  WHEN EXISTS (
    SELECT 1
    FROM (
        SELECT TOP 2 1 t
        FROM 
        (
            SELECT 1 ID

            UNION ALL

            SELECT TOP 1 ID 
            FROM dbo.X_HEAP 
            WHERE ID = 50000 
        ) h
        CROSS JOIN
        (
            SELECT 1 ID

            UNION ALL

            SELECT TOP 1 ID 
            FROM dbo.X_CI
            WHERE ID = 50000
        ) ci
    ) cnt
    HAVING COUNT(*) = 2
)
THEN 1 ELSE 0 END;

Puoi trovare una demo di db fiddle qui . La modifica dell'ordine delle tabelle derivate non modifica il piano di query. In entrambe le query la X_HEAPtabella non viene toccata. In altre parole, Query Optimizer sembra eseguire prima la query più economica. Non posso raccomandare di utilizzare qualcosa di simile in produzione, quindi è qui per il valore principalmente di curiosità. Potrebbe esserci un modo molto più semplice per ottenere lo stesso risultato.


4
O CASE WHEN EXISTS (SELECT 1 FROM dbo.X_CI WHERE ID = 500000 UNION ALL SELECT 1 FROM dbo.X_HEAP WHERE ID = 500000) THEN 1 ELSE 0 ENDpotrebbe essere un'alternativa, anche se ciò si basa ancora sulla decisione manuale di quale query è più veloce e su quella prima. Non sono sicuro se esiste un modo per esprimerlo in modo che SQL Server si riordini automaticamente in modo che quello economico venga valutato automaticamente per primo.
Martin Smith,
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.