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

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

+-------------+----------+---------+---------+--------------+
| 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.

+-------------+-----------+------------+-----------+--------------+
| 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