O
CASO
expression é uma das minhas construções favoritas em T-SQL. É bastante flexível e, às vezes, é a única maneira de controlar a ordem em que o SQL Server avaliará os predicados.No entanto, muitas vezes é mal interpretado.
O que é a expressão T-SQL CASE?
Em T-SQL,
CASE
é uma expressão que avalia uma ou mais expressões possíveis e retorna a primeira expressão apropriada. O termo expressão pode estar um pouco sobrecarregado aqui, mas basicamente é qualquer coisa que possa ser avaliada como um único valor escalar, como uma variável, uma coluna, um literal de string ou até mesmo a saída de uma função escalar ou embutida . Existem duas formas de CASE em T-SQL:
- Expressão CASE simples – quando você só precisa avaliar a igualdade:
CASE WHEN
THEN … [ELSE ] END
- Expressão CASE pesquisada – quando você precisa avaliar expressões mais complexas, como desigualdade, LIKE ou IS NOT NULL:
CASE WHEN
THEN … [ELSE ] END
A expressão de retorno é sempre um valor único e o tipo de dados de saída é determinado pela precedência do tipo de dados.
Como eu disse, a expressão CASE é muitas vezes mal compreendida; aqui estão alguns exemplos:
CASE é uma expressão, não uma instrução
Provavelmente não é importante para a maioria das pessoas, e talvez este seja apenas o meu lado pedante, mas muitas pessoas chamam isso de
CASE
declaração – incluindo a Microsoft, cuja documentação usa declaração e expressão alternadamente às vezes. Acho isso um pouco irritante (como linha/registro e coluna/campo ) e, embora seja principalmente semântica, mas há uma distinção importante entre uma expressão e uma instrução:uma expressão retorna um resultado. Quando as pessoas pensam em CASE
como uma declaração , isso leva a experimentos de encurtamento de código como este:SELECT CASE [status] WHEN 'A' THEN StatusLabel ='Autorizado', LastEvent =AuthorizedTime QUANDO 'C' THEN StatusLabel ='Concluído', LastEvent =CompletedTime ENDFROM dbo.some_table;
Ou isto:
SELECT CASE WHEN @foo =1 THEN (SELECT foo, bar FROM dbo.fizzbuzz)ELSE (SELECT blat, mort FROM dbo.splunge)END;
Este tipo de lógica de controle de fluxo pode ser possível com
CASE
declarações em outras linguagens (como VBScript), mas não no CASE
do Transact-SQL expressão . Para usar CASE
dentro da mesma lógica de consulta, você teria que usar um CASE
expressão para cada coluna de saída:SELECT StatusLabel =CASE [status] WHEN 'A' THEN 'Authorized' WHEN 'C' THEN 'Completed' END, LastEvent =CASE [status] WHEN 'A' THEN AuthorizedTime QUANDO 'C' THEN CompletedTime ENDFROM dbo.some_table;
CASE nem sempre causará curto-circuito
A documentação oficial uma vez implicou que toda a expressão entrará em curto-circuito, o que significa que avaliará a expressão da esquerda para a direita e parará de avaliar quando encontrar uma correspondência:
A instrução CASE [sic!] avalia suas condições sequencialmente e para na primeira condição cuja condição é satisfeita.
No entanto, isso nem sempre é verdade. E para seu crédito, em uma versão mais atual, a página passou a tentar explicar um cenário em que isso não é garantido. Mas isso só faz parte da história:
Em algumas situações, uma expressão é avaliada antes que uma instrução CASE [sic!] receba os resultados da expressão como entrada. Erros na avaliação dessas expressões são possíveis. As expressões agregadas que aparecem nos argumentos WHEN para uma instrução CASE [sic!] são avaliadas primeiro e, em seguida, fornecidas à instrução CASE [sic!]. Por exemplo, a consulta a seguir produz um erro de divisão por zero ao produzir o valor do agregado MAX. Isso ocorre antes de avaliar a expressão CASE.
O exemplo de divisão por zero é bem fácil de reproduzir, e eu demonstrei isso nesta resposta em dba.stackexchange.com:
DECLARE @i INT =1;SELECT CASE WHEN @i =1 THEN 1 ELSE MIN(1/0) END;
Resultado:
Msg 8134, Level 16, State 1
Dividir por zero erro encontrado.
Existem soluções triviais (como
ELSE (SELECT MIN(1/0)) END
), mas isso é uma verdadeira surpresa para muitos que não memorizaram as frases acima do Books Online. Tomei conhecimento deste cenário específico pela primeira vez em uma conversa em uma lista privada de distribuição de e-mail por Itzik Ben-Gan (@ItzikBenGan), que por sua vez foi inicialmente notificado por Jaime Lafargue. Eu relatei o bug no Connect #690017 :CASE / COALESCE nem sempre será avaliado em ordem textual; foi rapidamente fechado como "Por Design". Paul White (blog | @SQL_Kiwi) posteriormente arquivou Connect #691535 :Agregados não seguem a semântica do CASE, e foi fechado como "Corrigido". A correção, neste caso, foi um esclarecimento no artigo Books Online; ou seja, o trecho que eu copiei acima. Esse comportamento também pode ocorrer em alguns outros cenários menos óbvios. Por exemplo, Connect #780132 :FREETEXT() não respeita a ordem de avaliação em instruções CASE (sem agregações envolvidas) mostra que, bem,
CASE
também não é garantido que a ordem de avaliação seja da esquerda para a direita ao usar determinadas funções de texto completo. Nesse item, Paul White comentou que também observou algo semelhante usando o novo LAG()
função introduzida no SQL Server 2012. Não tenho uma reprodução à mão, mas acredito nele, e acho que não descobrimos todos os casos extremos em que isso pode ocorrer. Portanto, quando agregados ou serviços não nativos, como a pesquisa de texto completo, estiverem envolvidos, não faça suposições sobre curto-circuito em um
CASE
expressão. RAND() pode ser avaliado mais de uma vez
Muitas vezes vejo pessoas escrevendo um texto simples
CASO
expressão, assim:SELECT CASE @variable WHEN 1 THEN 'foo' WHEN 2 THEN 'bar'END
É importante entender que isso será executado como um pesquisado
CASO
expressão, assim:SELECT CASE WHEN @variable =1 THEN 'foo' WHEN @variable =2 THEN 'bar'END
A razão pela qual é importante entender que a expressão que está sendo avaliada será avaliada várias vezes, é porque ela pode realmente ser avaliada várias vezes. Quando isso é uma variável, uma constante ou uma referência de coluna, é improvável que seja um problema real; no entanto, as coisas podem mudar rapidamente quando é uma função não determinística. Considere que esta expressão produz um
SMALLINT
entre 1 e 3; vá em frente e execute-o muitas vezes, e você sempre obterá um desses três valores:SELECT CONVERT(SMALLINT, 1+RAND()*3);
Agora, coloque isso em um simples
CASE
expressão e execute-a uma dúzia de vezes - eventualmente você obterá um resultado de NULL
:SELECT [resultado] =CASE CONVERT(SMALLINT, 1+RAND()*3) WHEN 1 THEN 'one' WHEN 2 THEN 'two' WHEN 3 THEN 'three'END;
Como isso acontece? Bem, todo o
CASE
expressão é expandida para uma expressão pesquisada, da seguinte maneira:SELECT [resultado] =CASE WHEN CONVERT(SMALLINT, 1+RAND()*3) =1 THEN 'um' QUANDO CONVERT(SMALLINT, 1+RAND()*3) =2 THEN 'two' WHEN CONVERT( SMALLINT, 1+RAND()*3) =3 THEN 'three' ELSE NULL -- isso está sempre implicitamente láEND;
Por sua vez, o que acontece é que cada
QUANDO
cláusula avalia e invoca RAND()
independentemente – e em cada caso poderia render um valor diferente. Digamos que inserimos a expressão e verificamos o primeiro WHEN
cláusula, e o resultado é 3; pulamos essa cláusula e seguimos em frente. É concebível que as próximas duas cláusulas retornem 1 quando RAND()
é avaliada novamente – nesse caso, nenhuma das condições é avaliada como verdadeira, então o ELSE
assume. Outras expressões podem ser avaliadas mais de uma vez
Este problema não está limitado ao
RAND()
função. Imagine o mesmo estilo de não-determinismo vindo desses alvos móveis:SELECT [crypt_gen] =1+ABS(CRYPT_GEN_RANDOM(10) % 20), [newid] =LEFT(NEWID(),2), [checksum] =ABS(CHECKSUM(NEWID())%3);
Essas expressões obviamente podem gerar um valor diferente se avaliadas várias vezes. E com umCASE
pesquisado expressão, haverá momentos em que toda reavaliação cairá da pesquisa específica para oWHEN
atual , e, por fim, pressione oELSE
cláusula. Para se proteger disso, uma opção é sempre codificar seu próprioELSE
explícito.; apenas tome cuidado com o valor de fallback que você escolher retornar, porque isso terá algum efeito de distorção se você estiver procurando por uma distribuição uniforme. Outra opção é apenas alterar o últimoWHEN
cláusula paraELSE
, mas isso ainda levará a uma distribuição desigual. A opção preferida, na minha opinião, é tentar forçar o SQL Server a avaliar a condição uma vez (embora isso nem sempre seja possível em uma única consulta). Por exemplo, compare estes dois resultados:
-- Consulta A:expressão referenciada diretamente no CASE; não ELSE:SELECT x, COUNT(*) FROM( SELECT x =CASE ABS(CHECKSUM(NEWID())%3) WHEN 0 THEN '0' WHEN 1 THEN '1' WHEN 2 THEN '2' END FROM sys.all_columns ) AS y GRUPO POR x; -- Consulta B:cláusula ELSE adicional:SELECT x, COUNT(*) FROM( SELECT x =CASE ABS(CHECKSUM(NEWID())%3) WHEN 0 THEN '0' WHEN 1 THEN '1' WHEN 2 THEN '2 ' ELSE '2' END FROM sys.all_columns) AS y GROUP BY x; -- Consulta C:Final WHEN convertido para ELSE:SELECT x, COUNT(*) FROM( SELECT x =CASE ABS(CHECKSUM(NEWID())%3) WHEN 0 THEN '0' WHEN 1 THEN '1' ELSE '2 ' END FROM sys.all_columns) AS y GROUP BY x; -- Consulta D:Envie a avaliação de NEWID() para a subconsulta:SELECT x, COUNT(*) FROM( SELECT x =CASE x WHEN 0 THEN '0' WHEN 1 THEN '1' WHEN 2 THEN '2' END FROM ( SELECT x =ABS(CHECKSUM(NEWID())%3) FROM sys.all_columns ) AS x) AS y GROUP BY x;
Distribuição:
Valor | Consulta A | Consulta B | Consulta C | Consulta D |
---|---|---|---|---|
NULO | 2.572 | – | – | – |
0 | 2.923 | 2.900 | 2.928 | 2.949 |
1 | 1.946 | 1.959 | 1.927 | 2.896 |
2 | 1.295 | 3.877 | 3.881 | 2.891 |
Distribuição de valores com diferentes técnicas de consulta
Nesse caso, estou contando com o fato de que o SQL Server optou por avaliar a expressão na subconsulta e não apresentá-la ao
CASE
pesquisado expressão, mas isso é apenas para demonstrar que a distribuição pode ser coagida a ser mais uniforme. Na realidade, isso pode não ser sempre a escolha que o otimizador faz, então, por favor, não aprenda com este pequeno truque. :-) CHOOSE() também é afetado
Você observará que se você substituir o
CHECKSUM(NEWID())
expressão com o RAND()
expressão, você obterá resultados totalmente diferentes; mais notavelmente, o último retornará apenas um valor. Isso ocorre porque RAND()
, como GETDATE()
e algumas outras funções internas, recebe tratamento especial como uma constante de tempo de execução e é avaliada apenas uma vez por referência para toda a linha. Observe que ele ainda pode retornar NULL
assim como a primeira consulta no exemplo de código anterior. Este problema também não se limita ao
CASE
expressão; você pode ver um comportamento semelhante com outras funções internas que usam a mesma semântica subjacente. Por exemplo, ESCOLHER
é meramente açúcar sintático para um CASE
pesquisado mais elaborado expressão, e isso também produzirá NULL
ocasionalmente:SELECT [escolher] =ESCOLHER(CONVERT(SMALLINT, 1+RAND()*3),'um','dois','três');
IIF()
é uma função que eu esperava cair nessa mesma armadilha, mas essa função é realmente apenas um CASE
pesquisado expressão com apenas dois resultados possíveis e sem ELSE
– por isso é difícil, sem aninhar e introduzir outras funções, imaginar um cenário em que isso possa quebrar inesperadamente. Enquanto no caso simples é uma abreviação decente para CASE
, também é difícil fazer algo útil com ele se você precisar de mais de dois resultados possíveis. :-) COALESCE() também é afetado
Finalmente, devemos examinar que
COALESCE
pode ter problemas semelhantes. Vamos considerar que essas expressões são equivalentes:SELECT COALESCE(@variável, 'constante'); SELECT CASE WHEN @variable IS NOT NULL THEN @variable ELSE 'constant' END);
Nesse caso,
@variable
seria avaliada duas vezes (como qualquer função ou subconsulta, conforme descrito neste item do Connect). Eu realmente consegui obter alguns olhares confusos quando trouxe o exemplo a seguir em uma discussão recente no fórum. Digamos que eu queira preencher uma tabela com uma distribuição de valores de 1 a 5, mas sempre que um 3 for encontrado, quero usar -1. Não é um cenário muito real, mas fácil de construir e seguir. Uma maneira de escrever esta expressão é:
SELECT COALESCE(NULLIF(CONVERT(SMALLINT,1+RAND()*5),3),-1);
(Em inglês, trabalhando de dentro para fora:converta o resultado da expressão
1+RAND()*5
para um smallint; se o resultado dessa conversão for 3, defina-o como NULL
; se o resultado for NULL
, defina-o como -1. Você pode escrever isso com um CASE
mais detalhado expressão, mas conciso parece ser o rei.) Se você executar isso várias vezes, deverá ver um intervalo de valores de 1 a 5, além de -1. Você verá algumas instâncias de 3 e também deve ter notado que ocasionalmente vê
NULL
, embora você possa não esperar nenhum desses resultados. Vamos verificar a distribuição:USE tempdb;GOCREATE TABLE dbo.dist(TheNumber SMALLINT);GOINSERT dbo.dist(TheNumber) SELECT COALESCE(NULLIF(CONVERT(SMALLINT,1+RAND()*5),3),-1);GO 10000SELECT TheNumber, ocorrências =COUNT(*) FROM dbo.dist GROUP BY TheNumber ORDER BY TheNumber;GODROP TABLE dbo.dist;
Resultados (seus resultados certamente irão variar, mas a tendência básica deve ser semelhante):
O Número | ocorrências |
---|---|
NULO | 1.654 |
-1 | 2.002 |
1 | 1.290 |
2 | 1.266 |
3 | 1.287 |
4 | 1.251 |
5 | 1.250 |
Distribuição de TheNumber usando COALESCE
Decompondo uma expressão CASE pesquisada
Já está coçando a cabeça? Como os valores
NULL
e 3 aparecem, e por que a distribuição para NULL
e -1 substancialmente maior? Bem, responderei diretamente ao primeiro e convidarei hipóteses para o segundo. A expressão se expande aproximadamente para o seguinte, logicamente, pois
RAND()
é avaliado duas vezes dentro de NULLIF
e, em seguida, multiplique isso por duas avaliações para cada ramificação do COALESCE
função. Eu não tenho um depurador à mão, então isso não é necessariamente *exatamente* o que é feito dentro do SQL Server, mas deve ser equivalente o suficiente para explicar o ponto:SELECT CASE WHEN CASE WHEN CONVERT(SMALLINT,1+RAND()*5) =3 THEN NULL ELSE CONVERT(SMALLINT,1+RAND()*5) END NÃO É NULL THEN CASE WHEN CONVERT(SMALLINT,1+ RAND()*5) =3 THEN NULL ELSE CONVERT(SMALLINT,1+RAND()*5) END ELSE -1 ENDEND
Assim, você pode ver que ser avaliado várias vezes pode rapidamente se tornar um livro Choose Your Own Adventure™, e como ambos
NULL
e 3 são resultados possíveis que não parecem possíveis ao examinar a afirmação original. Uma observação interessante:isso não acontece da mesma forma se você pegar o script de distribuição acima e substituir COALESCE
com ISNULL
. Nesse caso, não há possibilidade de um NULL
saída; a distribuição é mais ou menos a seguinte:O Número | ocorrências |
---|---|
-1 | 1.966 |
1 | 1.585 |
2 | 1.644 |
3 | 1.573 |
4 | 1.598 |
5 | 1.634 |
Distribuição de TheNumber usando ISNULL
Novamente, seus resultados reais certamente variam, mas não devem muito. O ponto é que ainda podemos ver que 3 cai no esquecimento com bastante frequência, mas
ISNULL
elimina magicamente o potencial para NULL
para fazer todo o caminho. Falei sobre algumas das outras diferenças entre
COALESCE
e ISNULL
em uma dica, intitulada "Decidindo entre COALESCE e ISNULL no SQL Server". Quando escrevi isso, eu era fortemente a favor de usar COALESCE
excepto no caso em que o primeiro argumento foi uma subconsulta (de novo, devido a este Expressões CASE simples podem ser aninhadas em servidores vinculados
Uma das poucas limitações do
CASE
expressão é restrita a 10 níveis de aninhamento. Neste exemplo em dba.stackexchange.com, Paul White demonstra (usando o Plan Explorer) que uma expressão simples como esta:SELECT CASE column_name QUANDO '1' THEN 'a' QUANDO '2' THEN 'b' QUANDO '3' THEN 'c' ...ENDFROM ...
É expandido pelo analisador para o formulário pesquisado:
SELECT CASE WHEN column_name ='1' THEN 'a' WHEN column_name ='2' THEN 'b' WHEN column_name ='3' THEN 'c' ...ENDFROM ...
Mas na verdade pode ser transmitido por uma conexão de servidor vinculado como a seguinte consulta muito mais detalhada:
SELECT CASE WHEN column_name ='1' THEN 'a' ELSE CASE WHEN column_name ='2' THEN 'b' ELSE CASE WHEN column_name ='3' THEN 'c' ELSE ... ELSE NULL END END ENDFROM .. .
Nesta situação, mesmo que a consulta original tenha apenas um único
CASE
expressão com mais de 10 resultados possíveis, quando enviada para o servidor vinculado, tinha mais de 10 aninhados CASO
expressões. Como tal, como você poderia esperar, ele retornou um erro:Msg 8180, Level 16, State 1
Não foi possível preparar a(s) instrução(ões).
Msg 125, Level 15, State 4
As expressões case só podem ser aninhadas no nível 10.
Em alguns casos, você pode reescrevê-lo como Paul sugeriu, com uma expressão como esta (assumindo que
column_name
é uma coluna varchar):SELECT CASE CONVERT(VARCHAR(MAX), SUBSTRING(column_name, 1, 255)) QUANDO 'a' ENTÃO '1' QUANDO 'b' ENTÃO '2' QUANDO 'c' ENTÃO '3' ...ENDFROM . ..
Em alguns casos, apenas o
SUBSTRING
pode ser necessário alterar o local onde a expressão é avaliada; em outros, apenas o CONVERT
. Não realizei testes exaustivos, mas isso pode ter a ver com o provedor do servidor vinculado, opções como Collation Compatible e Use Remote Collation e a versão do SQL Server em cada extremidade do pipe. Para encurtar a história, é importante lembrar que seu
CASE
expressão pode ser reescrita para você sem aviso, e qualquer solução alternativa que você usar pode ser posteriormente anulada pelo otimizador, mesmo que funcione para você agora. Considerações Finais da Expressão do CASE e Recursos Adicionais
Espero ter dado algumas dicas sobre alguns dos aspectos menos conhecidos do
CASE
expressão e algumas informações sobre situações em que CASE
– e algumas das funções que usam a mesma lógica subjacente – retornam resultados inesperados. Alguns outros cenários interessantes em que esse tipo de problema surgiu:- Stack Overflow:como essa expressão CASE alcança a cláusula ELSE?
- Stack Overflow:CRYPT_GEN_RANDOM() Efeitos estranhos
- Stack Overflow:CHOOSE() não funciona como pretendido
- Stack Overflow:CHECKSUM(NewId()) é executado várias vezes por linha
- Conexão nº 350485 :Bug com NEWID() e expressões de tabela