O SQL Server lê toda uma função COALESCE, mesmo que o primeiro argumento não seja NULL?

98

Estou usando uma COALESCEfunção T-SQL em que o primeiro argumento não será nulo em cerca de 95% das vezes que é executado. Se o primeiro argumento for NULL, o segundo argumento é um processo bastante demorado:

SELECT COALESCE(c.FirstName
                ,(SELECT TOP 1 b.FirstName
                  FROM TableA a 
                  JOIN TableB b ON .....)
                )

Se, por exemplo, c.FirstName = 'John'o SQL Server ainda executasse a subconsulta?

Eu sei com a IIF()função VB.NET , se o segundo argumento for True, o código ainda lê o terceiro argumento (mesmo que não seja usado).

Curt
fonte

Respostas:

95

Não . Aqui está um teste simples:

SELECT COALESCE(1, (SELECT 1/0)) -- runs fine
SELECT COALESCE(NULL, (SELECT 1/0)) -- throws error

Se a segunda condição for avaliada, uma exceção será lançada para dividir por zero.

De acordo com a documentação do MSDN, isso está relacionado à maneira como COALESCEé visualizado pelo intérprete - é apenas uma maneira fácil de escrever uma CASEdeclaração.

CASE é conhecido por ser uma das únicas funções no SQL Server que (principalmente) causam um curto-circuito confiável.

Existem algumas exceções ao comparar variáveis ​​e agregações escalares, como mostrado por Aaron Bertrand em outra resposta aqui (e isso se aplica a CASEe COALESCE):

DECLARE @i INT = 1;
SELECT CASE WHEN @i = 1 THEN 1 ELSE MIN(1/0) END;

irá gerar uma divisão por erro zero.

Isso deve ser considerado um bug e, como regra, COALESCEserá analisado da esquerda para a direita.

JNK
fonte
6
@JNK, veja minha resposta para ver um caso muito simples, onde isso não é verdadeiro (minha preocupação é que haja ainda mais cenários ainda não descobertos - dificultando a concordância de que CASEsempre avalia os circuitos da esquerda para a direita e sempre os curtos )
Aaron Bertrand
4
Outro comportamento interessante @SQLKiwi me indicou: SELECT COALESCE((SELECT CASE WHEN RAND() <= 0.5 THEN 1 END), 1);- repetir várias vezes. Você receberá NULLalgumas vezes. Tente novamente com ISNULL- você nunca conseguirá NULL...
Aaron Bertrand
@ Martin sim, eu acredito que sim. Mas não o comportamento que a maioria dos usuários consideraria intuitivo, a menos que tivesse ouvido falar (ou ter sido mordido por) esse problema.
Aaron Bertrand
73

Que tal esse - como me foi relatado por Itzik Ben-Gan, que Jaime Lafargue falou sobre isso ?

DECLARE @i INT = 1;
SELECT CASE WHEN @i = 1 THEN 1 ELSE MIN(1/0) END;

Resultado:

Msg 8134, Level 16, State 1, Line 2
Divide by zero error encountered.

Existem soluções triviais, é claro, mas o ponto ainda é que CASEnem sempre garante a avaliação / curto-circuito da esquerda para a direita. Eu relatei o bug aqui e ele foi fechado como "por design". Paul White posteriormente arquivou este item do Connect e foi fechado como Fixo. Não porque foi corrigido per se, mas porque eles atualizaram os Manuais Online com uma descrição mais precisa do cenário em que agregados podem alterar a ordem de avaliação de uma CASEexpressão. Eu escrevi recentemente mais sobre isso aqui .

EDITE apenas um adendo, embora eu concorde que esses são casos extremos, que na maioria das vezes você pode confiar na avaliação da esquerda para a direita e em curto-circuito, e que esses são bugs que contradizem a documentação e provavelmente serão corrigidos ( isso não é definitivo - veja a conversa de acompanhamento no post de Bart Duncan no blog para saber o porquê). Tenho que discordar quando as pessoas dizem que algo sempre é verdadeiro, mesmo que exista um caso único que o refute. Se Itzik e outros podem encontrar bugs solitários como esse, torna pelo menos no campo da possibilidade que existem outros bugs também. E como não sabemos o restante da consulta do OP, não podemos dizer com certeza que ele confiará nesse curto-circuito, mas acabará sendo mordido por ele. Então, para mim, a resposta mais segura é:

Enquanto você pode geralmente dependem de CASEavaliar esquerda para a direita e de curto-circuito, conforme descrito na documentação, não é preciso dizer que você pode sempre fazê-lo. Há dois casos demonstrados nesta página em que isso não é verdade e nenhum bug foi corrigido em nenhuma versão publicamente disponível do SQL Server.

EDIT aqui é outro caso (eu preciso parar de fazer isso) em que uma CASEexpressão não é avaliada na ordem que você esperaria, mesmo que nenhum agregado esteja envolvido.

Aaron Bertrand
fonte
2
E parece que houve outro problema CASE que foi resolvido silenciosamente
Martin Smith
Na IMO, isso não prova que a avaliação da expressão CASE não é garantida porque os valores agregados são calculados antes da seleção (para que eles possam ser usados ​​dentro de um ter).
Salman A
1
@SalmanA Não sei ao certo o que mais isso faz, exceto provar exatamente que a ordem de avaliação em uma expressão CASE não é garantida. Estamos recebendo uma exceção porque o agregado é calculado primeiro, mesmo que esteja em uma cláusula ELSE que - se você seguir a documentação - nunca deve ser alcançada.
Aaron Bertrand
Os agregados @AaronBertrand são calculados antes da instrução CASE (e devem IMO). A documentação revisada indica exatamente isso, que o erro ocorre antes que o CASE seja avaliado.
Salman A
@SalmanA Ele ainda demonstra ao desenvolvedor casual que a expressão CASE não avalia na ordem em que foi escrita - a mecânica subjacente é irrelevante se tudo o que você está tentando fazer é entender por que um erro está vindo de um ramo CASE que não deveria ' não foram alcançados. Você também tem argumentos contra todos os outros exemplos desta página?
Aaron Bertrand
37

Minha opinião sobre isso é que a documentação deixa razoavelmente claro que a intenção é que o CASE entre em curto-circuito. Como Aaron menciona, houve vários casos (ha!) Em que isso demonstrou que nem sempre é verdade.

Até o momento, tudo isso foi reconhecido como bugs e corrigido - embora não necessariamente em uma versão do SQL Server que você possa comprar e consertar hoje (o bug de dobragem constante ainda não chegou ao AFAIK de atualização cumulativa). O novo bug em potencial - relatado originalmente por Itzik Ben-Gan - ainda não foi investigado (Aaron ou eu o adicionaremos ao Connect em breve).

Relacionadas à pergunta original, há outros problemas com CASE (e, portanto, COALESCE), em que funções ou subconsultas de efeito colateral são usadas. Considerar:

SELECT COALESCE((SELECT CASE WHEN RAND() <= 0.5 THEN 999 END), 999);
SELECT ISNULL((SELECT CASE WHEN RAND() <= 0.5 THEN 999 END), 999);

O formulário COALESCE geralmente retorna NULL, mais detalhes em https://connect.microsoft.com/SQLServer/feedback/details/546437/coalesce-subquery-1-may-return-null

Os problemas demonstrados com transformador otimizador e rastreamento de expressão comum significam que é impossível garantir que o CASE entre em curto-circuito em todas as circunstâncias. Posso conceber casos em que talvez nem seja possível prever o comportamento inspecionando a saída do plano de exibição pública, embora eu não tenha uma reprovação para isso hoje.

Em resumo, acho que você pode estar razoavelmente confiante de que o CASE entrará em curto-circuito em geral (principalmente se uma pessoa razoavelmente qualificada inspecionar o plano de execução e esse plano de execução for 'aplicado' com um guia ou dicas), mas se você precisar uma garantia absoluta, você deve escrever SQL que não inclua a expressão.

Não é uma situação extremamente satisfatória, eu acho.

Paul White
fonte
18

Já deparei com outro caso em que CASE/ COALESCEnão provo um curto-circuito. O TVF a seguir apresentará uma violação de PK se for passado 1como parâmetro.

CREATE FUNCTION F (@P INT)
RETURNS @T TABLE (
  C INT PRIMARY KEY)
AS
  BEGIN
      INSERT INTO @T
      VALUES      (1),
                  (@P)

      RETURN
  END

Se chamado da seguinte maneira

DECLARE @Number INT = 1

SELECT COALESCE(@Number, (SELECT number
                          FROM   master..spt_values
                          WHERE  type = 'P'
                                 AND number = @Number), 
                         (SELECT TOP (1)  C
                          FROM   F(@Number))) 

Ou como

DECLARE @Number INT = 1

SELECT CASE
         WHEN @Number = 1 THEN @Number
         ELSE (SELECT TOP (1) C
               FROM   F(@Number))
       END 

Ambos dão o resultado

Violação da restrição PRIMARY KEY 'PK__F__3BD019A800551192'. Não é possível inserir chave duplicada no objeto 'dbo. @ T'. O valor da chave duplicada é (1).

mostrando que a SELECT(ou pelo menos a população variável da tabela) ainda é executada e gera um erro, mesmo que esse ramo da instrução nunca deva ser alcançado. O plano para a COALESCEversão está abaixo.

Plano

Essa reescrita da consulta parece evitar o problema

SELECT COALESCE(Number, (SELECT number
                          FROM   master..spt_values
                          WHERE  type = 'P'
                                 AND number = Number), 
                         (SELECT TOP (1)  C
                          FROM   F(Number))) 
FROM (VALUES(1)) V(Number)   

O que dá plano

Plano2

Martin Smith
fonte
8

Outro exemplo

CREATE TABLE T1 (C INT PRIMARY KEY)

CREATE TABLE T2 (C INT PRIMARY KEY)

INSERT INTO T1 
OUTPUT inserted.* INTO T2
VALUES (1),(2),(3);

A pergunta

SET STATISTICS IO ON;

SELECT T1.C,
       COALESCE(T1.C , CASE WHEN EXISTS (SELECT * FROM T2 WHERE T2.C = T1.C)  THEN -1 END)
FROM T1
OPTION (LOOP JOIN)

Não mostra nenhuma leitura T2.

A busca de T2está sob uma passagem pelo predicado e o operador nunca é executado. Mas

SELECT T1.C,
       COALESCE(T1.C , CASE WHEN EXISTS (SELECT * FROM T2 WHERE T2.C = T1.C)  THEN -1 END)
FROM T1
OPTION (MERGE JOIN)

Será que mostram que T2é lido. Mesmo que nenhum valor T2seja realmente necessário.

É claro que isso não é realmente surpreendente, mas achei que vale a pena adicionar ao repositório de contra-exemplos, apenas porque levanta a questão do que significa curto-circuito em uma linguagem declarativa baseada em conjunto.

Martin Smith
fonte
7

Eu só queria mencionar uma estratégia que você pode não ter considerado. Pode não ser uma correspondência aqui, mas às vezes é útil. Veja se esta modificação oferece um desempenho melhor:

SELECT COALESCE(c.FirstName
            ,(SELECT TOP 1 b.FirstName
              FROM TableA a 
              JOIN TableB b ON .....
              WHERE C.FirstName IS NULL) -- this is the changed part
            )

Outra maneira de fazer isso pode ser isso (basicamente equivalente, mas permite acessar mais colunas da outra consulta, se necessário):

SELECT COALESCE(c.FirstName, x.FirstName)
FROM
   TableC c
   OUTER APPLY (
      SELECT TOP 1 b.FirstName
      FROM
         TableA a 
         JOIN TableB b ON ...
      WHERE
         c.FirstName IS NULL -- the important part
   ) x

Basicamente, essa é uma técnica de união de tabelas "rígida", mas inclui a condição de quando todas as linhas devem ser JOINed. Na minha experiência, isso realmente ajudou os planos de execução às vezes.

ErikE
fonte
3

Não, não seria. Seria executado apenas quando c.FirstNameé NULL.

No entanto, você deve tentar você mesmo. Experimentar. Você disse que sua subconsulta é longa. Referência. Tire suas próprias conclusões sobre isso.

A resposta do @Aaron na subconsulta em execução é mais completa.

No entanto, ainda acho que você deve refazer sua consulta e usar LEFT JOIN. Na maioria das vezes, as subconsultas podem ser removidas retrabalhando sua consulta para usar LEFT JOINs.

O problema com o uso de subconsultas é que sua instrução geral será executada mais lentamente porque a subconsulta é executada para cada linha no conjunto de resultados da consulta principal.

Adrian
fonte
@ Adrian ainda não está certo. Observe o plano de execução e verá que as subconsultas são frequentemente convertidas de maneira inteligente em JOINs. É um mero erro de experiência de pensamento supor que toda a subconsulta deve ser executada repetidamente para cada linha, embora isso possa ocorrer efetivamente se um loop aninhado se juntar a uma varredura.
ErikE
3

O padrão atual diz que todas as cláusulas WHEN (assim como a cláusula ELSE) devem ser analisadas para determinar o tipo de dados da expressão como um todo. Eu realmente teria que tirar algumas das minhas anotações antigas para determinar como um erro é tratado. Mas, por pouco, o 1/0 usa números inteiros, então eu presumo que, embora seja um erro. É um erro com o tipo de dados inteiro. Quando você tem apenas nulos na lista de coalescência, é um pouco mais difícil determinar o tipo de dados, e esse é outro problema.

Joe Celko
fonte