Sottoquery che utilizza Exists 1 o Exists *


88

Scrivevo i miei assegni EXISTS in questo modo:

IF EXISTS (SELECT * FROM TABLE WHERE Columns=@Filters)
BEGIN
   UPDATE TABLE SET ColumnsX=ValuesX WHERE Where Columns=@Filters
END

Uno degli amministratori di database in una vita precedente mi ha detto che quando faccio una EXISTSclausola, usa SELECT 1invece diSELECT *

IF EXISTS (SELECT 1 FROM TABLE WHERE Columns=@Filters)
BEGIN
   UPDATE TABLE SET ColumnsX=ValuesX WHERE Columns=@Filters
END

Questo fa davvero la differenza?


1
Hai dimenticato EXISTS (SELEZIONA NULL DA ...). Questo è stato chiesto di recente tra l'altro
OMG Ponies

16
ps ottenere un nuovo DBA. La superstizione non ha posto nell'IT, specialmente nella gestione del database (da un ex DBA !!!)
Matt Rogish

Risposte:


135

No, SQL Server è intelligente e sa di essere utilizzato per EXISTS e non restituisce NESSUN DATO al sistema.

Dice Microsoft: http://technet.microsoft.com/en-us/library/ms189259.aspx?ppud=4

L'elenco di selezione di una sottoquery introdotta da EXISTS è quasi sempre costituito da un asterisco (*). Non vi è alcun motivo per elencare i nomi delle colonne perché si sta solo verificando se esistono righe che soddisfano le condizioni specificate nella sottoquery.

Per controllarti, prova a eseguire quanto segue:

SELECT whatever
  FROM yourtable
 WHERE EXISTS( SELECT 1/0
                 FROM someothertable 
                WHERE a_valid_clause )

Se stesse effettivamente facendo qualcosa con l'elenco SELECT, genererebbe un div per errore zero. Non è così.

EDIT: Nota, lo standard SQL parla effettivamente di questo.

ANSI SQL 1992 Standard, pg 191 http://www.contrib.andrew.cmu.edu/~shadow/sql/sql1992.txt

3) Caso:
a) Se l ' <select list>"*" è semplicemente contenuto in un <subquery> che è immediatamente contenuto in un <exists predicate>, allora il <select list> è equivalente a un <value expression> che è un arbitrario <literal>.


1
il EXISTStrucco con 1/0 può essere esteso anche a questo SELECT 1 WHERE EXISTS(SELECT 1/0)... sembra un passaggio più astratto quindi dato che il secondo SELECTnon ha FROMclausole
whytheq

1
@whytheq - Or SELECT COUNT(*) WHERE EXISTS(SELECT 1/0). Un SELECTsenza un FROMin SQL Server viene trattato come se stesse accedendo a una tabella a riga singola (ad esempio simile alla selezione dalla dualtabella in altri RDBMS)
Martin Smith

@ MartinSmith applaude, quindi il punto è che SELECTcrea una tabella a 1 riga prima di fare qualsiasi altra cosa, quindi anche se 1/0la tabella a 1 riga è ancora spazzatura EXISTS?
whytheq

È sempre stato così o è un'ottimizzazione introdotta in una particolare versione di SQL Server?
Martin Brown,

1
@MartinSmith TIL "quoth". Grazie per averlo riparato.
Gurwinder Singh

111

La ragione di questo malinteso è presumibilmente a causa della convinzione che finirà per leggere tutte le colonne. È facile vedere che non è così.

CREATE TABLE T
(
X INT PRIMARY KEY,
Y INT,
Z CHAR(8000)
)

CREATE NONCLUSTERED INDEX NarrowIndex ON T(Y)

IF EXISTS (SELECT * FROM T)
    PRINT 'Y'

Dà un piano

Piano

Ciò mostra che SQL Server è stato in grado di utilizzare l'indice più stretto disponibile per verificare il risultato nonostante l'indice non includa tutte le colonne. L'accesso all'indice è sotto un operatore semi join, il che significa che può interrompere la scansione non appena viene restituita la prima riga.

Quindi è chiaro che la convinzione di cui sopra è sbagliata.

Tuttavia, Conor Cunningham del team Query Optimiser spiega qui che in genere utilizza SELECT 1in questo caso poiché può fare una piccola differenza di prestazioni nella compilazione della query.

Il QP prenderà ed espanderà tutti *i primi nella pipeline e li legherà agli oggetti (in questo caso, l'elenco delle colonne). Quindi rimuoverà le colonne non necessarie a causa della natura della query.

Quindi per una semplice EXISTSsottoquery come questa:

SELECT col1 FROM MyTable WHERE EXISTS (SELECT * FROM Table2 WHERE MyTable.col1=Table2.col2)Il *sarà ampliato a qualche potenzialmente grande elenco colonna e poi saranno determinati che la semantica del EXISTSnon richiede nessuna di queste colonne, quindi sostanzialmente tutti possono essere rimossi.

" SELECT 1" eviterà di dover esaminare i metadati non necessari per quella tabella durante la compilazione della query.

Tuttavia, in fase di esecuzione, le due forme della query saranno identiche e avranno tempi di esecuzione identici.

Ho testato quattro possibili modi per esprimere questa query su una tabella vuota con vari numeri di colonne. SELECT 1vs SELECT *vs SELECT Primary_Keyvs SELECT Other_Not_Null_Column.

Ho eseguito le query in un ciclo utilizzando OPTION (RECOMPILE)e misurato il numero medio di esecuzioni al secondo. Risultati di seguito

inserisci qui la descrizione dell'immagine

+-------------+----------+---------+---------+--------------+
| Num of Cols |    *     |    1    |   PK    | Not Null col |
+-------------+----------+---------+---------+--------------+
| 2           | 2043.5   | 2043.25 | 2073.5  | 2067.5       |
| 4           | 2038.75  | 2041.25 | 2067.5  | 2067.5       |
| 8           | 2015.75  | 2017    | 2059.75 | 2059         |
| 16          | 2005.75  | 2005.25 | 2025.25 | 2035.75      |
| 32          | 1963.25  | 1967.25 | 2001.25 | 1992.75      |
| 64          | 1903     | 1904    | 1936.25 | 1939.75      |
| 128         | 1778.75  | 1779.75 | 1799    | 1806.75      |
| 256         | 1530.75  | 1526.5  | 1542.75 | 1541.25      |
| 512         | 1195     | 1189.75 | 1203.75 | 1198.5       |
| 1024        | 694.75   | 697     | 699     | 699.25       |
+-------------+----------+---------+---------+--------------+
| Total       | 17169.25 | 17171   | 17408   | 17408        |
+-------------+----------+---------+---------+--------------+

Come si può vedere non esiste un vincitore coerente tra SELECT 1e SELECT *e la differenza tra i due approcci è trascurabile. Le SELECT Not Null cole SELECT PKappaiono però leggermente più veloci.

Le prestazioni di tutte e quattro le query diminuiscono all'aumentare del numero di colonne nella tabella.

Poiché la tabella è vuota, questa relazione sembra spiegabile solo dalla quantità di metadati della colonna. Perché COUNT(1)è facile vedere che questo viene riscritto COUNT(*)a un certo punto del processo dal basso.

SET SHOWPLAN_TEXT ON;

GO

SELECT COUNT(1)
FROM master..spt_values

Che dà il seguente piano

  |--Compute Scalar(DEFINE:([Expr1003]=CONVERT_IMPLICIT(int,[Expr1004],0)))
       |--Stream Aggregate(DEFINE:([Expr1004]=Count(*)))
            |--Index Scan(OBJECT:([master].[dbo].[spt_values].[ix2_spt_values_nu_nc]))

Collegamento di un debugger al processo di SQL Server e interruzione casuale durante l'esecuzione di quanto segue

DECLARE @V int 

WHILE (1=1)
    SELECT @V=1 WHERE EXISTS (SELECT 1 FROM ##T) OPTION(RECOMPILE)

Ho scoperto che nei casi in cui la tabella ha 1.024 colonne il più delle volte lo stack di chiamate assomiglia a qualcosa di simile al seguente, indicando che sta effettivamente impiegando gran parte del tempo a caricare i metadati della colonna anche quando SELECT 1viene utilizzato (per il caso in cui il la tabella ha 1 colonna che si interrompe in modo casuale non ha raggiunto questo bit dello stack di chiamate in 10 tentativi)

sqlservr.exe!CMEDAccess::GetProxyBaseIntnl()  - 0x1e2c79 bytes  
sqlservr.exe!CMEDProxyRelation::GetColumn()  + 0x57 bytes   
sqlservr.exe!CAlgTableMetadata::LoadColumns()  + 0x256 bytes    
sqlservr.exe!CAlgTableMetadata::Bind()  + 0x15c bytes   
sqlservr.exe!CRelOp_Get::BindTree()  + 0x98 bytes   
sqlservr.exe!COptExpr::BindTree()  + 0x58 bytes 
sqlservr.exe!CRelOp_FromList::BindTree()  + 0x5c bytes  
sqlservr.exe!COptExpr::BindTree()  + 0x58 bytes 
sqlservr.exe!CRelOp_QuerySpec::BindTree()  + 0xbe bytes 
sqlservr.exe!COptExpr::BindTree()  + 0x58 bytes 
sqlservr.exe!CScaOp_Exists::BindScalarTree()  + 0x72 bytes  
... Lines omitted ...
msvcr80.dll!_threadstartex(void * ptd=0x0031d888)  Line 326 + 0x5 bytes C
kernel32.dll!_BaseThreadStart@8()  + 0x37 bytes 

Questo tentativo di profiling manuale è supportato dal profiler del codice VS 2012 che mostra una selezione molto diversa di funzioni che consumano il tempo di compilazione per i due casi ( Prime 15 funzioni 1024 colonne vs Prime 15 funzioni 1 colonna ).

Entrambe le versioni SELECT 1e SELECT *finiscono per controllare le autorizzazioni delle colonne e falliscono se all'utente non viene concesso l'accesso a tutte le colonne nella tabella.

Un esempio che ho preso da una conversazione sul mucchio

CREATE USER blat WITHOUT LOGIN;
GO
CREATE TABLE dbo.T
(
X INT PRIMARY KEY,
Y INT,
Z CHAR(8000)
)
GO

GRANT SELECT ON dbo.T TO blat;
DENY SELECT ON dbo.T(Z) TO blat;
GO
EXECUTE AS USER = 'blat';
GO

SELECT 1
WHERE  EXISTS (SELECT 1
               FROM   T); 
/*  ↑↑↑↑ 
Fails unexpectedly with 

The SELECT permission was denied on the column 'Z' of the 
           object 'T', database 'tempdb', schema 'dbo'.*/

GO
REVERT;
DROP USER blat
DROP TABLE T

Quindi si potrebbe ipotizzare che la piccola differenza apparente durante l'utilizzo SELECT some_not_null_colè che finisce solo per controllare le autorizzazioni su quella colonna specifica (sebbene carichi ancora i metadati per tutti). Tuttavia, questo non sembra adattarsi ai fatti poiché la differenza percentuale tra i due approcci se qualcosa si riduce all'aumentare del numero di colonne nella tabella sottostante.

In ogni caso non mi affretterò a modificare tutte le mie query in questo modulo poiché la differenza è molto minore e apparente solo durante la compilazione delle query. La rimozione di in OPTION (RECOMPILE)modo che le esecuzioni successive possano utilizzare un piano memorizzato nella cache ha fornito quanto segue.

inserisci qui la descrizione dell'immagine

+-------------+-----------+------------+-----------+--------------+
| Num of Cols |     *     |     1      |    PK     | Not Null col |
+-------------+-----------+------------+-----------+--------------+
| 2           | 144933.25 | 145292     | 146029.25 | 143973.5     |
| 4           | 146084    | 146633.5   | 146018.75 | 146581.25    |
| 8           | 143145.25 | 144393.25  | 145723.5  | 144790.25    |
| 16          | 145191.75 | 145174     | 144755.5  | 146666.75    |
| 32          | 144624    | 145483.75  | 143531    | 145366.25    |
| 64          | 145459.25 | 146175.75  | 147174.25 | 146622.5     |
| 128         | 145625.75 | 143823.25  | 144132    | 144739.25    |
| 256         | 145380.75 | 147224     | 146203.25 | 147078.75    |
| 512         | 146045    | 145609.25  | 145149.25 | 144335.5     |
| 1024        | 148280    | 148076     | 145593.25 | 146534.75    |
+-------------+-----------+------------+-----------+--------------+
| Total       | 1454769   | 1457884.75 | 1454310   | 1456688.75   |
+-------------+-----------+------------+-----------+--------------+

Lo script di test che ho usato può essere trovato qui


3
+1 Questa risposta merita più voti positivi per lo sforzo richiesto per ottenere dati reali.
Jon

1
Qualche idea su quale versione di SQL Server sono state generate queste statistiche?
Martin Brown,

3
@MartinBrown - IIRC originariamente nel 2008 anche se ho rifatto i test di recente nel 2012 per la modifica più recente e ho trovato lo stesso.
Martin Smith,

8

Il modo migliore per saperlo è testare le prestazioni di entrambe le versioni e controllare il piano di esecuzione per entrambe le versioni. Scegli una tabella con molte colonne.


2
+1. Non ho idea del motivo per cui questo è stato votato. Ho sempre pensato che fosse meglio insegnare a un uomo a pescare, piuttosto che dargli semplicemente un pesce. Come faranno le persone a imparare qualcosa?
Ogre Psalm33

5

Non c'è differenza in SQL Server e non è mai stato un problema in SQL Server. L'ottimizzatore sa che sono la stessa cosa. Se guardi i piani di esecuzione, vedrai che sono identici.


1

Personalmente trovo molto, molto difficile credere che non si ottimizzino per lo stesso piano di query. Ma l'unico modo per sapere nella tua situazione particolare è testarlo. Se lo fai, per favore fai rapporto!


-1

Nessuna differenza reale, ma potrebbe esserci un piccolo calo delle prestazioni. Come regola generale, non dovresti chiedere più dati del necessario.

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.