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 1
in 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 EXISTS
sottoquery 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
EXISTS
non 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 1
vs SELECT *
vs SELECT Primary_Key
vs 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 1
e SELECT *
e la differenza tra i due approcci è trascurabile. Le SELECT Not Null col
e SELECT PK
appaiono 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 1
viene 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 1
e 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