Database
 sql >> Base de Dados >  >> RDS >> Database

Segredos Sujos da Expressão CASE


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 um CASE pesquisado expressão, haverá momentos em que toda reavaliação cairá da pesquisa específica para o WHEN atual , e, por fim, pressione o ELSE cláusula. Para se proteger disso, uma opção é sempre codificar seu próprio ELSE 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 último WHEN cláusula para ELSE , 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 bug "lacuna de recurso"). Agora não tenho tanta certeza se me sinto tão forte sobre isso.

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