Subconsulta usando Exists 1 ou Exists *

88

Eu costumava escrever meus cheques EXISTS assim:

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

Um dos DBAs em uma vida anterior me disse que quando faço uma EXISTScláusula, uso em SELECT 1vez deSELECT *

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

Isso realmente faz diferença?

Raj More
fonte
1
Você esqueceu EXISTS (SELECT NULL FROM ...). Isso foi perguntado recentemente
Pôneis OMG,
16
ps obter um novo DBA. Superstição não tem lugar em TI, especialmente em gerenciamento de banco de dados (de um ex-DBA !!!)
Matt Rogish

Respostas:

135

Não, o SQL Server é inteligente e sabe que está sendo usado para um EXISTS e não retorna NENHUM DADO ao sistema.

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

A lista de seleção de uma subconsulta introduzida por EXISTS quase sempre consiste em um asterisco (*). Não há razão para listar nomes de colunas porque você está apenas testando se existem linhas que atendem às condições especificadas na subconsulta.

Para verificar você mesmo, tente executar o seguinte:

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

Se ele estivesse realmente fazendo algo com a lista SELECT, ele lançaria um erro div por zero. Não é verdade.

EDIT: Nota, o SQL Standard realmente fala sobre isso.

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

3) Caso:
a) Se o <select list>"*" está simplesmente contido em a <subquery> que está imediatamente contido em um <exists predicate>, então o <select list> é equivalente a um <value expression> que é arbitrário <literal>.

Matt Rogish
fonte
1
o EXISTStruque com 1/0 pode até mesmo ser estendido para isso SELECT 1 WHERE EXISTS(SELECT 1/0)... parece um passo mais abstrato do que o segundo SELECTnão tem FROMcláusula
theq
1
@whytheq - Ou SELECT COUNT(*) WHERE EXISTS(SELECT 1/0). Um SELECTsem um FROMno SQL Server é tratado como se estivesse acessando uma tabela de linha única (por exemplo, semelhante à seleção da dualtabela em outros RDBMSs)
Martin Smith
@MartinSmith cheers - então o ponto é que SELECTcria uma tabela de 1 linha antes de fazer qualquer outra coisa, mesmo que 1/0a tabela de 1 linha ainda seja lixo EXISTS?
Whytheq
Sempre foi assim ou é uma otimização introduzida em uma versão específica do SQL Server?
Martin Brown
1
@MartinSmith TIL "quoth". Obrigado por consertar isso.
Gurwinder Singh
111

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

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

insira a descrição da imagem aqui

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

insira a descrição da imagem aqui

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

Martin Smith
fonte
3
+1 Esta resposta merece mais votos pelo esforço envolvido para obter dados reais.
Jon
1
Alguma ideia de qual versão do SQL Server essas estatísticas foram geradas?
Martin Brown
3
@MartinBrown - IIRC originalmente em 2008, embora eu refiz os testes recentemente em 2012 para a edição mais recente e achei o mesmo.
Martin Smith
8

A melhor maneira de saber é testar o desempenho de ambas as versões e verificar o plano de execução para ambas as versões. Escolha uma tabela com muitas colunas.

HLGEM
fonte
2
+1. Não tenho ideia de por que isso foi rejeitado. Sempre achei melhor ensinar um homem a pescar do que simplesmente dar-lhe um peixe. Como as pessoas vão aprender alguma coisa?
Ogre Salmo 33
5

Não há diferença no SQL Server e nunca foi um problema no SQL Server. O otimizador sabe que eles são iguais. Se você olhar os planos de execução, verá que são idênticos.

Cade Roux
fonte
1

Pessoalmente, acho muito difícil acreditar que eles não otimizem para o mesmo plano de consulta. Mas a única maneira de saber em sua situação particular é testando. Se você fizer isso, por favor, relate!

Larry Lustig
fonte
-1

Nenhuma diferença real, mas pode haver um impacto muito pequeno no desempenho. Como regra geral, você não deve pedir mais dados do que precisa.

Orjan
fonte