it-swarm.it

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

Sto usando una funzione T-SQL COALESCE in cui 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 comunque la query secondaria?

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

102
Curt

No . 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.

Per Documentazione MSDN questo è legato al modo in cui COALESCE viene visualizzato dall'interprete - è solo un modo semplice per scrivere un'istruzione CASE.

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 CASE che 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 regola COALESCE analizzerà da sinistra a destra.

96
JNK

Che ne dici di questo - secondo quanto riferito da Itzik Ben-Gan, che era 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.

Ci sono soluzioni banali, ovviamente, ma il punto è ancora che CASE non sempre garantisce la valutazione/cortocircuito da sinistra a destra. Ho segnalato il bug qui ed è stato chiuso come "di progettazione". Paul White ha successivamente presentato questo elemento Connect ed è stato chiuso come Risolto. 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 cambiare l'ordine di valutazione di un'espressione CASE. Recentemente ho scritto di più sul blog qui .

[~ # ~] edit [~ # ~] solo un addendum, mentre sono d'accordo che si tratta di casi Edge, che la maggior parte dei tempo puoi fare affidamento sulla valutazione da sinistra a destra e sul corto circuito e che si tratta di bug che contraddicono la documentazione e che probabilmente verranno risolti (questo non è definito - vedi la conversazione di follow-up su post sul blog di Bart Duncan per capire perché), non sono d'accordo quando la gente dice che qualcosa è sempre vero anche se c'è un singolo caso Edge 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 sia possibile di solito fare affidamento su CASE per valutare da sinistra a destra e corto circuito, come descritto nella documentazione, non è preciso affermare che si può sempre fare così. 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 [~ # ~] ecco un altro caso (Devo smettere di farlo) dove un L'espressione CASE non viene valutata nell'ordine previsto, anche se non sono coinvolti aggregati.

75
Aaron Bertrand

La documentazione chiarisce che l'intenzione è di CASE in corto circuito. Come menzioni di Aaron , ci sono stati diversi casi segnalati in cui questo ha dimostrato di non essere sempre vero. Finora, la maggior parte di questi sono stati riconosciuti come bug e risolti.

Esistono altri problemi con CASE (e quindi COALESCE) in cui vengono utilizzate funzioni con effetti collaterali o query secondarie. Prendere in considerazione:

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 spesso restituisce null, come descritto in na segnalazione di bug di Hugo Kornelis.

I problemi dimostrati con le trasformazioni dell'ottimizzatore e il tracciamento delle espressioni comuni indicano che è impossibile garantire che CASE cortocircuiti in tutte le circostanze.

Penso che tu possa essere ragionevolmente fiducioso che CASE cortocircuita in generale (in particolare se una persona sufficientemente qualificata ispeziona il piano di esecuzione e che il piano di esecuzione viene 'applicato' con una guida o suggerimenti per il piano) ma se hai bisogno di una garanzia assoluta, devi scrivere SQL che non includa affatto l'espressione.

38
Paul White 9

Mi sono imbattuto in un altro caso in cui CASE/COALESCE non fa corto circuito. Il seguente TVF genererà una violazione PK se superato 1 come 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).

mostrando che SELECT (o almeno la popolazione variabile della tabella) viene ancora eseguito e genera un errore anche se non si dovrebbe mai raggiungere quel ramo dell'istruzione. Il piano per la versione COALESCE è di seguito.

Plan

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

20
Martin Smith

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 affatto.

La ricerca di T2 è sottoposto a predicato pass-through 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 mostra che T2 viene letto. Anche se nessun valore da T2 è mai realmente necessario.

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.

9
Martin Smith

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 per unire tabelle "dure" ma includendo la condizione su quando tutte le righe dovrebbero essere JOINed. Nella mia esperienza, questo ha aiutato a volte i piani di esecuzione.

7
ErikE

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 tirare 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.

3
Joe Celko

No, non lo sarebbe. Funzionerebbe solo quando 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 @Aaron sulla sottoquery in esecuzione è più completa.

Tuttavia, penso ancora che dovresti rielaborare la tua query e utilizzare LEFT JOIN. Il più 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.

2
Adriano Carneiro