SQL Server legge tutte le funzioni COALESCE anche se il primo argomento non è NULL?


98

Sto usando una funzione T-SQL in COALESCEcui il primo argomento non sarà nullo su circa il 95% delle volte che viene eseguito. Se il primo argomento è NULL, il secondo argomento è un processo piuttosto lungo:

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

Se, ad esempio, c.FirstName = 'John'SQL Server eseguisse ancora la query secondaria?

Conosco la IIF()funzione VB.NET , se il secondo argomento è True, il codice legge ancora il terzo argomento (anche se non verrà utilizzato).

Risposte:


95

Nope . Ecco un semplice test:

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

Se viene valutata la seconda condizione, viene generata un'eccezione per la divisione per zero.

Secondo la documentazione MSDN, ciò è correlato al modo in cui COALESCEviene interpretato dall'interprete: è solo un modo semplice per scrivere una CASEdichiarazione.

CASE è ben noto per essere una delle uniche funzioni in SQL Server che (principalmente) corti in modo affidabile.

Ci sono alcune eccezioni quando si confrontano con variabili scalari e aggregazioni come mostrato da Aaron Bertrand in un'altra risposta qui (e questo si applicherebbe sia a CASEche COALESCE):

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

genererà una divisione per zero errori.

Questo dovrebbe essere considerato un bug e di norma COALESCEanalizzerà da sinistra a destra.


6
@JNK, per favore, vedi la mia risposta per vedere un caso molto semplice in cui questo non è vero (la mia preoccupazione è che ci sono ancora più scenari ancora da scoprire - rendendo difficile concordare che CASEvaluta sempre da sinistra a destra e sempre cortocircuiti ).
Aaron Bertrand

4
Altri comportamenti interessanti @SQLKiwi mi hanno indicato: SELECT COALESCE((SELECT CASE WHEN RAND() <= 0.5 THEN 1 END), 1);- ripetere più volte. Otterrai a NULLvolte. Riprova con ISNULL- non otterrai mai NULL...
Aaron Bertrand


@Martin sì, ci credo. Ma non un comportamento che la maggior parte degli utenti troverebbe intuitivo se non avessero sentito parlare (o essere stato morso da) quel problema.
Aaron Bertrand

73

Che ne dici di questo - come mi è stato riferito da Itzik Ben-Gan, che mi è stato raccontato da Jaime Lafargue ?

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

Risultato:

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

Esistono ovviamente soluzioni banali, ma il punto è che CASEnon sempre garantisce valutazione / cortocircuito da sinistra a destra. Ho segnalato il bug qui ed è stato chiuso come "di progettazione". Paul White ha successivamente archiviato questo elemento Connect , che è stato chiuso come fisso. Non perché è stato risolto di per sé, ma perché hanno aggiornato la documentazione online con una descrizione più accurata dello scenario in cui gli aggregati possono modificare l'ordine di valutazione di CASEun'espressione. Di recente ho scritto di più sul blog qui .

MODIFICA solo un addendum, mentre concordo sul fatto che si tratta di casi limite, che la maggior parte delle volte puoi fare affidamento su valutazione e cortocircuito da sinistra a destra e che si tratta di bug che contraddicono la documentazione e probabilmente verranno risolti ( questo non è definito - vedi la conversazione di follow-up sul post del blog di Bart Duncan per capire perché), non sono d'accordo quando la gente dice che qualcosa è sempre vero anche se c'è un caso unico che lo smentisce. Se Itzik e altri riescono a trovare bug solitari come questo, lo rende almeno nel regno della possibilità che ci siano anche altri bug. E poiché non conosciamo il resto della query del PO, non possiamo dire con certezza che farà affidamento su questo corto circuito ma finirà per essere morso da esso. Quindi per me la risposta più sicura è:

Sebbene di solito si possa fare affidamento CASEper valutare da sinistra a destra e corto circuito, come descritto nella documentazione, non è corretto affermare che è sempre possibile farlo. Esistono due casi dimostrati in questa pagina in cui non è vero e nessuno dei due bug è stato corretto in alcuna versione di SQL Server disponibile pubblicamente.

EDIT qui è un altro caso (devo smettere di farlo) in cui CASEun'espressione non valuta nell'ordine che ti aspetteresti, anche se non sono coinvolti aggregati.


2
E sembra che ci sia stato un altro problema CASE che è stato risolto tranquillamente
Martin Smith il

IMO questo non dimostra che la valutazione dell'espressione CASE non sia garantita perché i valori aggregati vengono calcolati prima di selezionare (in modo che possano essere utilizzati all'interno di avere).
Salman A

1
@SalmanA Non sono sicuro di cos'altro possa fare se non dimostrare esattamente che l'ordine di valutazione in un'espressione CASE non è garantito. Stiamo ricevendo un'eccezione perché l'aggregato viene calcolato per primo, anche se è incluso in una clausola ELSE che, se si osserva la documentazione, non dovrebbe mai essere raggiunto.
Aaron Bertrand

Gli aggregati @AaronBertrand sono calcolati prima dell'istruzione CASE (e dovrebbero essere IMO). La documentazione rivista sottolinea esattamente questo, che l'errore si verifica prima della valutazione di CASE.
Salman A

@SalmanA Dimostra ancora allo sviluppatore occasionale che l'espressione CASE non valuta nell'ordine in cui è stata scritta - i meccanismi sottostanti sono irrilevanti se tutto ciò che stai cercando di fare è capire perché un ramo CASE proviene da un errore che non dovrebbe ' sono stati raggiunti. Hai argomenti anche su tutti gli altri esempi in questa pagina?
Aaron Bertrand

37

La mia opinione su questo è che la documentazione chiarisca ragionevolmente che l' intenzione è che CASE sia in corto circuito. Come menziona Aaron, ci sono stati diversi casi (ha!) In cui questo ha dimostrato di non essere sempre vero.

Finora, tutti questi sono stati riconosciuti come bug e risolti, anche se non necessariamente in una versione di SQL Server è possibile acquistare e correggere oggi (il bug a costante costante non è ancora arrivato a un aggiornamento cumulativo AFAIK). L'ultimo potenziale bug - originariamente segnalato da Itzik Ben-Gan - deve ancora essere esaminato (o Aaron o lo aggiungeremo a Connect a breve).

Relativamente alla domanda originale, ci sono altri problemi con CASE (e quindi COALESCE) in cui vengono utilizzate funzioni con effetti collaterali o sottoquery. Tener conto di:

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

Il modulo COALESCE restituisce spesso NULL, maggiori dettagli su https://connect.microsoft.com/SQLServer/feedback/details/546437/coalesce-subquery-1-may-return-null

I problemi dimostrati con le trasformazioni dell'ottimizzatore e il tracciamento delle espressioni comuni significano che è impossibile garantire che CASE eseguirà il cortocircuito in tutte le circostanze. Sono in grado di concepire casi in cui potrebbe non essere nemmeno possibile prevedere il comportamento ispezionando l'output del piano dello spettacolo pubblico, anche se oggi non ho una replica per questo.

In sintesi, penso che tu possa essere ragionevolmente fiducioso che CASE eseguirà un cortocircuito in generale (in particolare se una persona abbastanza qualificata ispeziona il piano di esecuzione e che il piano di esecuzione viene 'applicato' con una guida o suggerimenti per il piano) ma se è necessario una garanzia assoluta, devi scrivere SQL che non includa affatto l'espressione.

Non uno stato di cose estremamente soddisfacente, immagino.


18

Mi sono imbattuto in un altro caso in cui CASE/ COALESCEnon corto circuito. Il seguente TVF genererà una violazione PK se passato 1come parametro.

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

      RETURN
  END

Se chiamato come segue

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

O come

DECLARE @Number INT = 1

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

Entrambi danno il risultato

Violazione del vincolo PRIMARY KEY 'PK__F__3BD019A800551192'. Impossibile inserire la chiave duplicata nell'oggetto 'dbo. @ T'. Il valore della chiave duplicata è (1).

dimostrando che la SELECT(o almeno la popolazione variabile della tabella) viene ancora eseguita e genera un errore anche se quel ramo dell'istruzione non dovrebbe mai essere raggiunto. Il piano per la COALESCEversione è di seguito.

Piano

Questa riscrittura della query sembra evitare il 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)   

Che dà piano

Plan2


8

Un altro esempio

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);

La domanda

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)

Non mostra alcuna lettura contro T2.

La ricerca di T2è sotto un passaggio attraverso predicato e l'operatore non viene mai eseguito. Ma

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)

Fa spettacolo che T2viene letto. Anche se in T2realtà non è mai necessario alcun valore da .

Naturalmente questo non è davvero sorprendente, ma ho pensato che valesse la pena aggiungerlo al repository del contro esempio, anche solo perché solleva il problema di cosa significhi il corto circuito in un linguaggio dichiarativo basato su set.


7

Volevo solo menzionare una strategia che potresti non aver considerato. Potrebbe non essere una partita qui, ma a volte è utile. Verifica se questa modifica offre prestazioni migliori:

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
            )

Un altro modo per farlo potrebbe essere questo (sostanzialmente equivalente, ma consente di accedere a più colonne dall'altra query se necessario):

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

Fondamentalmente questa è una tecnica di "hard" join di tabelle ma include la condizione su quando tutte le righe dovrebbero essere JOINed. Nella mia esperienza, questo ha aiutato a volte i piani di esecuzione.


3

No, non lo sarebbe. Funzionerebbe solo quando lo c.FirstNameè NULL.

Tuttavia, dovresti provarlo tu stesso. Sperimentare. Hai detto che la tua subquery è lunga. Prova delle prestazioni. Trai le tue conclusioni su questo.

La risposta di @Aaron sulla sottoquery in esecuzione è più completa.

Tuttavia, continuo a pensare che dovresti rielaborare la query e utilizzarla LEFT JOIN. La maggior parte delle volte, le query secondarie possono essere rimosse rielaborando la query per utilizzare LEFT JOINs.

Il problema con l'utilizzo di query secondarie è che l'istruzione generale verrà eseguita più lentamente perché la query secondaria viene eseguita per ogni riga nel set di risultati della query principale.


@Adrian non è ancora giusto. Guarda il piano di esecuzione e vedrai che le sottoquery vengono spesso convertite in modo abbastanza intelligente in JOIN. È un semplice errore dell'esperimento mentale supporre che l'intera sottoquery debba essere ripetuta più volte per ogni riga, sebbene ciò possa effettivamente accadere se si sceglie un join di loop nidificato con una scansione.
ErikE

3

Lo standard attuale afferma che tutte le clausole WHEN (così come la clausola ELSE) devono essere analizzate per determinare il tipo di dati dell'espressione nel suo insieme. Dovrei davvero tirar fuori alcune delle mie vecchie note per determinare come viene gestito un errore. Ma appena fuori mano, 1/0 usa numeri interi, quindi suppongo che sia un errore. È un errore con il tipo di dati intero. Quando hai solo null nella lista di coalescenza, è un po 'più complicato determinare il tipo di dati, e questo è un altro problema.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.