Il CASE
expression è uno dei miei costrutti preferiti in T-SQL. È abbastanza flessibile e talvolta è l'unico modo per controllare l'ordine in cui SQL Server valuterà i predicati.
Tuttavia, è spesso frainteso.
Cos'è l'espressione CASE T-SQL?
In T-SQL, CASE
è un'espressione che valuta una o più espressioni possibili e restituisce la prima espressione appropriata. Il termine espressione potrebbe essere un po' sovraccaricato qui, ma fondamentalmente è qualsiasi cosa che può essere valutata come un singolo valore scalare, come una variabile, una colonna, una stringa letterale o anche l'output di una funzione integrata o scalare .
Esistono due forme di CASE in T-SQL:
- Espressione CASE semplice – quando devi solo valutare l'uguaglianza:
CASE <input> WHEN <eval> THEN <return> … [ELSE <return>] END
- Espressione CASE cercata – quando devi valutare espressioni più complesse, come disuguaglianza, LIKE o IS NOT NULL:
CASE WHEN <input_bool> THEN <return> … [ELSE <return>] END
L'espressione restituita è sempre un valore singolo e il tipo di dati di output è determinato dalla precedenza del tipo di dati.
Come ho detto, l'espressione CASE è spesso fraintesa; ecco alcuni esempi:
CASE è un'espressione, non un'affermazione
Probabilmente non è importante per la maggior parte delle persone, e forse questo è solo il mio lato pedante, ma molte persone lo chiamano un CASE
dichiarazione – inclusa Microsoft, la cui documentazione utilizza dichiarazione e espressione a volte in modo intercambiabile. Trovo questo leggermente fastidioso (come riga/record e colonna/campo ) e, sebbene sia principalmente semantica, ma esiste un'importante distinzione tra un'espressione e un'istruzione:un'espressione restituisce un risultato. Quando la gente pensa a CASE
come dichiarazione , porta a esperimenti di accorciamento del codice come questo:
SELECT CASE [status] WHEN 'A' THEN StatusLabel = 'Authorized', LastEvent = AuthorizedTime WHEN 'C' THEN StatusLabel = 'Completed', LastEvent = CompletedTime END FROM dbo.some_table;
O questo:
SELECT CASE WHEN @foo = 1 THEN (SELECT foo, bar FROM dbo.fizzbuzz) ELSE (SELECT blat, mort FROM dbo.splunge) END;
Questo tipo di logica di controllo del flusso può essere possibile con CASE
dichiarazioni in altri linguaggi (come VBScript), ma non in CASE
di Transact-SQL espressione . Per utilizzare CASE
all'interno della stessa logica di query, dovresti usare un CASE
espressione per ogni colonna di output:
SELECT StatusLabel = CASE [status] WHEN 'A' THEN 'Authorized' WHEN 'C' THEN 'Completed' END, LastEvent = CASE [status] WHEN 'A' THEN AuthorizedTime WHEN 'C' THEN CompletedTime END FROM dbo.some_table;
CASE non va sempre in cortocircuito
La documentazione ufficiale una volta implicava che l'intera espressione andasse in cortocircuito, il che significa che valuterà l'espressione da sinistra a destra e smetterà di valutare quando raggiunge una corrispondenza:
L'istruzione CASE [sic!] valuta le sue condizioni in sequenza e si ferma con la prima condizione la cui condizione è soddisfatta.Tuttavia, questo non è sempre vero. E a suo merito, in una versione più attuale, la pagina ha cercato di spiegare uno scenario in cui ciò non è garantito. Ma entra solo in parte nella storia:
In alcune situazioni, un'espressione viene valutata prima che un'istruzione CASE [sic!] riceva i risultati dell'espressione come input. Sono possibili errori nella valutazione di queste espressioni. Le espressioni aggregate che appaiono negli argomenti WHEN di un'istruzione CASE [sic!] vengono prima valutate, quindi fornite all'istruzione CASE [sic!]. Ad esempio, la query seguente produce un errore di divisione per zero durante la produzione del valore dell'aggregazione MAX. Ciò si verifica prima di valutare l'espressione CASE.L'esempio di divisione per zero è abbastanza facile da riprodurre e l'ho dimostrato in questa risposta su dba.stackexchange.com:
DECLARE @i INT = 1; SELECT CASE WHEN @i = 1 THEN 1 ELSE MIN(1/0) END;
Risultato:
Msg 8134, livello 16, stato 1Si è verificato un errore di divisione per zero.
Esistono soluzioni alternative banali (come ELSE (SELECT MIN(1/0)) END
), ma questa è una vera sorpresa per molti che non hanno memorizzato le frasi di cui sopra da Books Online. Sono stato informato per la prima volta di questo scenario specifico in una conversazione su una lista di distribuzione di posta elettronica privata da Itzik Ben-Gan (@ItzikBenGan), che a sua volta è stato inizialmente informato da Jaime Lafargue. Ho segnalato il bug in Connect #690017:CASE / COALESCE non sempre valuta in ordine testuale; è stato rapidamente chiuso come "By Design". Paul White (blog | @SQL_Kiwi) ha successivamente depositato Connect #691535:Gli aggregati non seguono la semantica di CASE ed è stato chiuso come "Risolto". La correzione, in questo caso, è stata un chiarimento nell'articolo della documentazione in linea; vale a dire, lo snippet che ho copiato sopra.
Questo comportamento può manifestarsi anche in altri scenari meno ovvi. Ad esempio, Connect #780132 :FREETEXT() non rispetta l'ordine di valutazione nelle istruzioni CASE (nessun aggregato coinvolto) mostra che, beh, CASE
Non è garantito che l'ordine di valutazione sia da sinistra a destra quando si utilizzano determinate funzioni full-text. Su quell'elemento, Paul White ha commentato di aver anche osservato qualcosa di simile usando il nuovo LAG()
funzione introdotta in SQL Server 2012. Non ho una riproduzione a portata di mano, ma gli credo e non credo che abbiamo portato alla luce tutti i casi limite in cui ciò potrebbe verificarsi.
Pertanto, quando sono coinvolti servizi aggregati o non nativi come la ricerca di testo completo, non fare supposizioni sul cortocircuito in un CASE
espressione.
RAND() può essere valutato più di una volta
Vedo spesso persone che scrivono un semplice CASE
espressione, come questa:
SELECT CASE @variable WHEN 1 THEN 'foo' WHEN 2 THEN 'bar' END
È importante capire che questo verrà eseguito come cercato CASE
espressione, come questa:
SELECT CASE WHEN @variable = 1 THEN 'foo' WHEN @variable = 2 THEN 'bar' END
Il motivo per cui è importante capire che l'espressione valutata verrà valutata più volte è perché può essere effettivamente valutata più volte. Quando si tratta di una variabile, di una costante o di un riferimento di colonna, è improbabile che si tratti di un problema reale; tuttavia, le cose possono cambiare rapidamente quando si tratta di una funzione non deterministica. Considera che questa espressione produce un SMALLINT
tra 1 e 3; vai avanti ed eseguilo molte volte e otterrai sempre uno di questi tre valori:
SELECT CONVERT(SMALLINT, 1+RAND()*3);
Ora, inseriscilo in un semplice CASE
expression ed eseguilo una dozzina di volte:alla fine otterrai un risultato di NULL
:
SELECT [result] = CASE CONVERT(SMALLINT, 1+RAND()*3) WHEN 1 THEN 'one' WHEN 2 THEN 'two' WHEN 3 THEN 'three' END;
Come succede? Bene, l'intero CASE
l'espressione viene espansa in un'espressione cercata, come segue:
SELECT [result] = CASE WHEN CONVERT(SMALLINT, 1+RAND()*3) = 1 THEN 'one' WHEN CONVERT(SMALLINT, 1+RAND()*3) = 2 THEN 'two' WHEN CONVERT(SMALLINT, 1+RAND()*3) = 3 THEN 'three' ELSE NULL -- this is always implicitly there END;
A sua volta, ciò che accade è che ogni WHEN
la clausola valuta e invoca RAND()
indipendentemente – e in ogni caso potrebbe produrre un valore diverso. Diciamo che inseriamo l'espressione e controlliamo il primo WHEN
clausola, e il risultato è 3; saltiamo quella clausola e andiamo avanti. È ipotizzabile che le due clausole successive restituiscano entrambe 1 quando RAND()
viene valutato di nuovo, nel qual caso nessuna delle condizioni viene valutata come vera, quindi ELSE
prende il sopravvento.
Altre espressioni possono essere valutate più di una volta
Questo problema non è limitato al RAND()
funzione. Immagina lo stesso stile di non determinismo proveniente da questi bersagli mobili:
SELECT [crypt_gen] = 1+ABS(CRYPT_GEN_RANDOM(10) % 20), [newid] = LEFT(NEWID(),2), [checksum] = ABS(CHECKSUM(NEWID())%3);
Queste espressioni possono ovviamente produrre un valore diverso se valutate più volte. E con un CASE
cercato espressione, ci saranno momenti in cui ogni rivalutazione esce dalla ricerca specifica per l'attuale WHEN
e infine premi ELSE
clausola. Per proteggerti da questo, un'opzione è quella di codificare sempre il tuo ELSE
esplicito; fai solo attenzione al valore di fallback che scegli di restituire, perché questo avrà un effetto di distorsione se stai cercando una distribuzione uniforme. Un'altra opzione è semplicemente cambiare l'ultimo WHEN
clausola su ELSE
, ma ciò comporterà comunque una distribuzione non uniforme. L'opzione preferita, secondo me, è provare a costringere SQL Server a valutare la condizione una volta (sebbene ciò non sia sempre possibile all'interno di una singola query). Ad esempio, confronta questi due risultati:
-- Query A: expression referenced directly in CASE; no 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 GROUP BY x; -- Query B: additional ELSE clause: 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; -- Query C: Final WHEN converted to 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; -- Query D: Push evaluation of NEWID() to subquery: 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;
Distribuzione:
Valore | Richiesta A | Interrogazione B | interrogazione C | Richiesta D |
---|---|---|---|---|
NULLO | 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 |
Distribuzione di valori con diverse tecniche di query
In questo caso, mi sto basando sul fatto che SQL Server ha scelto di valutare l'espressione nella sottoquery e di non introdurla nel CASE
cercato espressione, ma questo serve semplicemente a dimostrare che la distribuzione può essere costretta ad essere più uniforme. In realtà, questa potrebbe non essere sempre la scelta che fa l'ottimizzatore, quindi per favore non imparare da questo piccolo trucco. :-)
Anche CHOOSE() è interessato
Osserverai che se sostituisci CHECKSUM(NEWID())
espressione con RAND()
espressione, otterrai risultati completamente diversi; in particolare, quest'ultimo restituirà sempre e solo un valore. Questo perché RAND()
, come GETDATE()
e alcune altre funzioni integrate, riceve un trattamento speciale come costante di runtime e viene valutata solo una volta per riferimento per l'intera riga. Nota che può comunque restituire NULL
proprio come la prima query nell'esempio di codice precedente.
Anche questo problema non è limitato al CASE
espressione; puoi vedere un comportamento simile con altre funzioni integrate che utilizzano la stessa semantica sottostante. Ad esempio, CHOOSE
è semplicemente zucchero sintattico per un CASE
più elaborato espressione, e questo produrrà anche NULL
occasionalmente:
SELECT [choose] = CHOOSE(CONVERT(SMALLINT, 1+RAND()*3),'one','two','three');
IIF()
è una funzione in cui mi aspettavo di cadere nella stessa trappola, ma questa funzione è in realtà solo un CASE
cercato espressione con solo due possibili risultati e nessun ELSE
– quindi è difficile, senza annidare e introdurre altre funzioni, immaginare uno scenario in cui questo possa interrompersi inaspettatamente. Mentre nel caso semplice è un'abbreviazione decente per CASE
, è anche difficile fare qualcosa di utile con esso se hai bisogno di più di due possibili risultati. :-)
Anche COALESCE() è interessato
Infine, dovremmo esaminare quel COALESCE
può avere problemi simili. Consideriamo che queste espressioni sono equivalenti:
SELECT COALESCE(@variable, 'constant'); SELECT CASE WHEN @variable IS NOT NULL THEN @variable ELSE 'constant' END);
In questo caso, @variable
verrebbe valutato due volte (come qualsiasi funzione o sottoquery, come descritto in questo elemento Connect).
Sono stato davvero in grado di ottenere alcuni sguardi perplessi quando ho portato il seguente esempio in una recente discussione sul forum. Diciamo che voglio popolare una tabella con una distribuzione di valori da 1 a 5, ma ogni volta che si incontra un 3, voglio invece usare -1. Non uno scenario molto reale, ma facile da costruire e seguire. Un modo per scrivere questa espressione è:
SELECT COALESCE(NULLIF(CONVERT(SMALLINT,1+RAND()*5),3),-1);
(In inglese, lavorando dall'interno:converti il risultato dell'espressione 1+RAND()*5
a una piccola fetta; se il risultato di tale conversione è 3, impostalo su NULL
; se il risultato è NULL
, impostalo su -1. Potresti scriverlo con un CASE
più dettagliato espressione, ma conciso sembra essere il re.)
Se lo esegui un sacco di volte, dovresti vedere un intervallo di valori compreso tra 1-5 e -1. Vedrai alcune istanze di 3 e potresti anche aver notato che occasionalmente vedi NULL
, anche se potresti non aspettarti nessuno di questi risultati. Controlliamo la distribuzione:
USE tempdb; GO CREATE TABLE dbo.dist(TheNumber SMALLINT); GO INSERT dbo.dist(TheNumber) SELECT COALESCE(NULLIF(CONVERT(SMALLINT,1+RAND()*5),3),-1); GO 10000 SELECT TheNumber, occurences = COUNT(*) FROM dbo.dist GROUP BY TheNumber ORDER BY TheNumber; GO DROP TABLE dbo.dist;
Risultati (i tuoi risultati varieranno sicuramente, ma la tendenza di base dovrebbe essere simile):
Il numero | occorrenze |
---|---|
NULL | 1.654 |
-1 | 2.002 |
1 | 1.290 |
2 | 1.266 |
3 | 1.287 |
4 | 1.251 |
5 | 1.250 |
Distribuzione di TheNumber utilizzando COALESCE
Scomposizione di un'espressione CASE cercata
Ti stai già grattando la testa? Come funzionano i valori NULL
e 3 vengono visualizzati, e perché la distribuzione per NULL
e -1 sostanzialmente superiore? Bene, risponderò direttamente alla prima e inviterò ipotesi per la seconda.
L'espressione si espande approssimativamente a quanto segue, logicamente, poiché RAND()
viene valutato due volte all'interno di NULLIF
, quindi moltiplicalo per due valutazioni per ogni ramo di COALESCE
funzione. Non ho un debugger a portata di mano, quindi questo non è necessariamente *esattamente* ciò che viene fatto all'interno di SQL Server, ma dovrebbe essere abbastanza equivalente per spiegare il punto:
SELECT CASE WHEN CASE WHEN CONVERT(SMALLINT,1+RAND()*5) = 3 THEN NULL ELSE CONVERT(SMALLINT,1+RAND()*5) END IS NOT NULL THEN CASE WHEN CONVERT(SMALLINT,1+RAND()*5) = 3 THEN NULL ELSE CONVERT(SMALLINT,1+RAND()*5) END ELSE -1 END END
Quindi puoi vedere che essere valutato più volte può diventare rapidamente un libro Scegli la tua avventura™ e come entrambi NULL
e 3 sono possibili esiti che non sembrano possibili quando si esamina l'affermazione originale. Una nota a margine interessante:questo non accade esattamente se prendi lo script di distribuzione sopra e sostituisci COALESCE
con ISNULL
. In tal caso, non è possibile un NULL
produzione; la distribuzione è approssimativamente la seguente:
Il numero | occorrenze |
---|---|
-1 | 1.966 |
1 | 1.585 |
2 | 1.644 |
3 | 1.573 |
4 | 1.598 |
5 | 1.634 |
Distribuzione di TheNumber utilizzando ISNULL
Ancora una volta, i tuoi risultati effettivi varieranno sicuramente, ma non dovrebbero variare di molto. Il punto è che possiamo ancora vedere che 3 cade abbastanza spesso, ma ISNULL
elimina magicamente il potenziale per NULL
per arrivare fino in fondo.
Ho parlato di alcune delle altre differenze tra COALESCE
e ISNULL
in un suggerimento, intitolato "Decidere tra COALESCE e ISNULL in SQL Server". Quando l'ho scritto, ero decisamente favorevole all'utilizzo di COALESCE
tranne nel caso in cui il primo argomento fosse una sottoquery (di nuovo, a causa di questo bug "gap di funzionalità"). Ora non sono così sicuro di esserne così forte.
Semplici espressioni CASE possono essere nidificate su server collegati
Una delle poche limitazioni del CASE
l'espressione è limitata a 10 livelli di nido. In questo esempio su dba.stackexchange.com, Paul White dimostra (usando Plan Explorer) che un'espressione semplice come questa:
SELECT CASE column_name WHEN '1' THEN 'a' WHEN '2' THEN 'b' WHEN '3' THEN 'c' ... END FROM ...
Viene espanso dal parser al modulo cercato:
SELECT CASE WHEN column_name = '1' THEN 'a' WHEN column_name = '2' THEN 'b' WHEN column_name = '3' THEN 'c' ... END FROM ...
Ma può effettivamente essere trasmesso su una connessione al server collegata come la seguente query molto più dettagliata:
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 END FROM ...
In questa situazione, anche se la query originale aveva un solo CASE
espressione con 10+ risultati possibili, quando inviata al server collegato aveva 10+ nidificati CASE
espressioni. Pertanto, come ci si potrebbe aspettare, ha restituito un errore:
Impossibile preparare le dichiarazioni.
Msg 125, livello 15, stato 4
Le espressioni dei casi possono essere nidificate solo al livello 10.
In alcuni casi, puoi riscriverlo come suggerito da Paul, con un'espressione come questa (assumendo column_name
è una colonna varchar):
SELECT CASE CONVERT(VARCHAR(MAX), SUBSTRING(column_name, 1, 255)) WHEN 'a' THEN '1' WHEN 'b' THEN '2' WHEN 'c' THEN '3' ... END FROM ...
In alcuni casi, solo il SUBSTRING
potrebbe essere richiesto di modificare la posizione in cui viene valutata l'espressione; in altri, solo il CONVERT
. Non ho eseguito test esaurienti, ma ciò potrebbe avere a che fare con il provider del server collegato, opzioni come Collation Compatible e Use Remote Collation e la versione di SQL Server alle due estremità della pipe.
Per farla breve, è importante ricordare che il tuo CASE
l'espressione può essere riscritta per te senza preavviso e che qualsiasi soluzione alternativa utilizzata potrebbe essere successivamente annullata dall'ottimizzatore, anche se ora funziona per te.
Espressione del CASO Considerazioni finali e risorse aggiuntive
Spero di aver dato qualche spunto di riflessione su alcuni degli aspetti meno noti del CASE
espressione e alcune informazioni sulle situazioni in cui CASE
– e alcune delle funzioni che utilizzano la stessa logica sottostante – restituiscono risultati imprevisti. Alcuni altri scenari interessanti in cui è emerso questo tipo di problema:
- Stack Overflow:in che modo questa espressione CASE raggiunge la clausola ELSE?
- Overflow dello stack:CRYPT_GEN_RANDOM() strani effetti
- Overflow dello stack:CHOOSE() non funziona come previsto
- Overflow dello stack:CHECKSUM(NewId()) viene eseguito più volte per riga
- Connetti #350485 :Bug con NEWID() ed espressioni di tabella