A razão para esse equívoco é provavelmente devido à crença de que ele acabará lendo todas as colunas. É fácil perceber que não é esse o caso.
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á plano

Isso mostra que o SQL Server foi capaz de usar o índice mais estreito disponível para verificar o resultado, apesar de o índice não incluir todas as colunas. O acesso ao índice está sob um operador semi join, o que significa que ele pode interromper a varredura assim que a primeira linha for retornada.
Portanto, está claro que a crença acima está errada.
No entanto, Conor Cunningham, da equipe do Query Optimizer, explica aqui que ele normalmente usa SELECT 1neste caso, pois pode fazer uma pequena diferença de desempenho na compilação da consulta.
O QP pegará e expandirá todos *os primeiros no pipeline e os vinculará aos objetos (neste caso, a lista de colunas). Em seguida, removerá as colunas desnecessárias devido à natureza da consulta.
Portanto, para uma EXISTSsubconsulta simples como esta:
SELECT col1 FROM MyTable WHERE EXISTS
(SELECT * FROM Table2 WHERE
MyTable.col1=Table2.col2)O *será expandido para alguma lista de colunas potencialmente grande e então será determinado que a semântica do
EXISTS não requer nenhuma dessas colunas, portanto, basicamente, todas elas podem ser removidas.
"SELECT 1 " evitará ter que examinar quaisquer metadados desnecessários para aquela tabela durante a compilação da consulta.
No entanto, em tempo de execução, as duas formas da consulta serão idênticas e terão tempos de execução idênticos.
Testei quatro maneiras possíveis de expressar essa consulta em uma tabela vazia com vários números de colunas. SELECT 1vs SELECT *vs SELECT Primary_Keyvs SELECT Other_Not_Null_Column.
Executei as consultas em um loop usando OPTION (RECOMPILE)e medindo o número médio de execuções por segundo. Resultados abaixo

+-------------+----------+---------+---------+--------------+
| 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 |
+-------------+----------+---------+---------+--------------+
Como pode ser visto, não há um vencedor consistente entre SELECT 1e SELECT *e a diferença entre as duas abordagens é insignificante. O SELECT Not Null cole SELECT PKparece um pouco mais rápido.
Todas as quatro consultas diminuem de desempenho conforme o número de colunas na tabela aumenta.
Como a tabela está vazia, essa relação parece explicável apenas pela quantidade de metadados da coluna. Pois COUNT(1)é fácil ver que isso foi reescrito COUNT(*)em algum ponto do processo a partir de baixo.
SET SHOWPLAN_TEXT ON;
GO
SELECT COUNT(1)
FROM master..spt_values
O que dá o seguinte plano
|--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]))
Anexar um depurador ao processo do SQL Server e interromper aleatoriamente durante a execução do seguinte
DECLARE @V int
WHILE (1=1)
SELECT @V=1 WHERE EXISTS (SELECT 1 FROM ##T) OPTION(RECOMPILE)
Descobri que nos casos em que a tabela tem 1.024 colunas na maioria das vezes, a pilha de chamadas se parece com algo como o abaixo, indicando que está realmente gastando uma grande proporção do tempo carregando metadados de coluna, mesmo quando SELECT 1é usado (para o caso em que o a tabela tem 1 coluna que quebra aleatoriamente não atingiu esta parte da pilha de chamadas em 10 tentativas)
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
Esta tentativa manual de criação de perfil é apoiada pelo criador de perfil de código do VS 2012, que mostra uma seleção muito diferente de funções consumindo o tempo de compilação para os dois casos ( 15 principais funções 1024 colunas vs 15 principais funções 1 coluna ).
As versões SELECT 1e SELECT *terminam verificando as permissões da coluna e falham se o usuário não tiver acesso a todas as colunas da tabela.
Um exemplo que tirei de uma conversa na pilha
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
Portanto, pode-se especular que a menor diferença aparente ao usar SELECT some_not_null_col é que ele apenas verifica as permissões naquela coluna específica (embora ainda carregue os metadados para todos). No entanto, isso não parece se adequar aos fatos, já que a diferença percentual entre as duas abordagens, se alguma coisa fica menor, conforme o número de colunas na tabela subjacente aumenta.
Em qualquer caso, não irei me apressar e alterar todas as minhas consultas para este formulário, pois a diferença é muito pequena e apenas aparente durante a compilação da consulta. A remoção do OPTION (RECOMPILE)para que as execuções subsequentes possam usar um plano em cache forneceu o seguinte.

+-------------+-----------+------------+-----------+--------------+
| 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 |
+-------------+-----------+------------+-----------+--------------+
O script de teste que usei pode ser encontrado aqui