Alias ​​de referência (calculado em SELECT) na cláusula WHERE

130
SELECT (InvoiceTotal - PaymentTotal - CreditTotal) AS BalanceDue
FROM Invoices
WHERE BalanceDue > 0 --error

O valor calculado 'BalanceDue' definido como uma variável na lista de colunas selecionadas não pode ser usado na cláusula WHERE.

Existe uma maneira que ele pode? Nesta questão relacionada ( Usando uma variável no MySQL Select Statment em uma Cláusula Where ), parece que a resposta seria, na verdade, não, você apenas escreveria o cálculo ( e executaria esse cálculo na consulta) duas vezes, nenhum dos o que é satisfatório.

Nicholas Petersen
fonte

Respostas:

237

Você não pode fazer referência a um alias, exceto em ORDER BY, porque SELECT é a segunda última cláusula avaliada. Duas soluções alternativas:

SELECT BalanceDue FROM (
  SELECT (InvoiceTotal - PaymentTotal - CreditTotal) AS BalanceDue
  FROM Invoices
) AS x
WHERE BalanceDue > 0;

Ou apenas repita a expressão:

SELECT (InvoiceTotal - PaymentTotal - CreditTotal) AS BalanceDue
FROM Invoices
WHERE  (InvoiceTotal - PaymentTotal - CreditTotal)  > 0;

Eu prefiro o último. Se a expressão for extremamente complexa (ou dispendiosa de calcular), provavelmente você deve considerar uma coluna computada (e talvez persistente), especialmente se muitas consultas se referirem a essa mesma expressão.

PS seus medos parecem infundados. Neste exemplo simples, pelo menos, o SQL Server é inteligente o suficiente para executar o cálculo apenas uma vez, mesmo que você o tenha mencionado duas vezes. Vá em frente e compare os planos; você verá que são idênticos. Se você tem um caso mais complexo em que vê a expressão avaliada várias vezes, publique a consulta e os planos mais complexos.

Aqui estão 5 exemplos de consultas que produzem exatamente o mesmo plano de execução:

SELECT LEN(name) + column_id AS x
FROM sys.all_columns
WHERE LEN(name) + column_id > 30;

SELECT x FROM (
SELECT LEN(name) + column_id AS x
FROM sys.all_columns
) AS x
WHERE x > 30;

SELECT LEN(name) + column_id AS x
FROM sys.all_columns
WHERE column_id + LEN(name) > 30;

SELECT name, column_id, x FROM (
SELECT name, column_id, LEN(name) + column_id AS x
FROM sys.all_columns
) AS x
WHERE x > 30;

SELECT name, column_id, x FROM (
SELECT name, column_id, LEN(name) + column_id AS x
FROM sys.all_columns
) AS x
WHERE LEN(name) + column_id > 30;

Plano resultante para todas as cinco consultas:

insira a descrição da imagem aqui

Aaron Bertrand
fonte
11
Uau. SQL Server é inteligente o suficiente para executar somente o cálculo uma vez
alternatefaraz
5
Uau, isso é uma resposta de alta qualidade!
Siddhartha
Eu precisava de alguns condicionais extras em uma declaração MERGE, e essa era a única maneira de fazê-lo funcionar. Obrigado!
Eric Burdo 11/07/19
1
@EricBurdo Se você estiver usando MERGE, certifique-se de ter levado tudo isso em consideração: MERGEcom cuidado .
Aaron Bertrand
11

Você pode fazer isso usando cross apply

SELECT c.BalanceDue AS BalanceDue
FROM Invoices
cross apply (select (InvoiceTotal - PaymentTotal - CreditTotal) as BalanceDue) as c
WHERE  c.BalanceDue  > 0;
Manoj
fonte
4

Na verdade, é possível definir efetivamente uma variável que pode ser usada nas cláusulas SELECT, WHERE e outras.

Uma junção cruzada não permite necessariamente a ligação apropriada às colunas da tabela referenciada, no entanto OUTER APPLY permite - e trata os nulos de maneira mais transparente.

SELECT
    vars.BalanceDue
FROM
    Entity e
OUTER APPLY (
    SELECT
        -- variables   
        BalanceDue = e.EntityTypeId,
        Variable2 = ...some..long..complex..expression..etc...
    ) vars
WHERE
    vars.BalanceDue > 0

Muitos elogios para Syed Mehroz Alam .

Peter Aylett
fonte