SQL Server lit-il l'intégralité d'une fonction COALESCE même si le premier argument n'est pas NULL?


98

J'utilise une fonction T-SQL dans COALESCElaquelle le premier argument ne sera pas nul environ 95% de son exécution. Si le premier argument est NULL, le second argument est un processus assez long:

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

Si, par exemple, c.FirstName = 'John'SQL Server exécutait-il toujours la sous-requête?

Je sais qu'avec la IIF()fonction VB.NET , si le deuxième argument est True, le code lit toujours le troisième argument (même s'il ne sera pas utilisé).

Réponses:


95

Nope . Voici un test simple:

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

Si la deuxième condition est évaluée, une exception est générée pour la division par zéro.

Selon la documentation MSDN, cela est lié à la façon dont COALESCEl'interprète visualise le message - il s'agit simplement d'un moyen simple d'écrire une CASEinstruction.

CASE est bien connu pour être l’une des seules fonctions de SQL Server qui (la plupart du temps) court-circuite de manière fiable.

Il existe quelques exceptions lors de la comparaison avec des variables scalaires et des agrégations, comme l'a montré Aaron Bertrand dans une autre réponse ici (et cela s'appliquerait à la fois à CASEet COALESCE):

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

va générer une division par zéro erreur.

Cela devrait être considéré comme un bug et, en règle générale, COALESCEsera analysé de gauche à droite.


6
@JNK s'il vous plaît voir ma réponse pour voir un cas très simple où cela ne se vérifie pas (mon inquiétude est qu'il existe encore plus de scénarios encore non découverts - il est difficile de s'entendre sur le fait que l' CASEon évalue toujours les circuits de gauche à droite et toujours courts. ).
Aaron Bertrand

4
Autre comportement intéressant @SQLKiwi m'a signalé: SELECT COALESCE((SELECT CASE WHEN RAND() <= 0.5 THEN 1 END), 1);- répéter plusieurs fois. Vous aurez NULLparfois. Réessayez avec ISNULL- vous n'aurez jamais NULL...
Aaron Bertrand


@ Martin oui je crois bien. Mais pas un comportement que la plupart des utilisateurs trouveraient intuitif à moins d’avoir entendu parler de (ou avoir été piqué par) de ce problème.
Aaron Bertrand

73

Qu'en est-il de celui-ci - comme cela m'a été rapporté par Itzik Ben-Gan, à qui Jaime Lafargue en a parlé ?

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

Résultat:

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

Il existe bien sûr des solutions de contournement triviales, mais le fait est que CASEcela ne garantit pas toujours une évaluation / un court-circuit de gauche à droite. J'ai signalé le bug ici et il a été fermé comme "par la conception." Paul White a ultérieurement classé cet élément Connect , qui a été fermé comme étant corrigé. Non pas parce que cela a été corrigé en soi, mais parce qu'ils ont mis à jour la documentation en ligne avec une description plus précise du scénario dans lequel les agrégats peuvent modifier l'ordre d'évaluation d'une CASEexpression. J'ai récemment blogué plus à ce sujet ici .

EDIT juste un addendum, bien que je convienne que ce sont des cas extrêmes, que la plupart du temps, vous pouvez vous fier à une évaluation de gauche à droite et à des courts-circuits, et que ce sont des bogues contrariant la documentation et susceptibles d'être corrigés ( ce n'est pas certain - consultez la conversation de suivi sur le blog de Bart Duncan pour savoir pourquoi), je ne suis pas d'accord quand des gens disent que quelque chose est toujours vrai même s'il y a un seul cas qui le réfute. Si Itzik et d’autres peuvent trouver des bogues solitaires comme celui-ci, il est au moins possible qu’il y ait d’autres bogues. Et comme nous ne connaissons pas le reste de la requête du PO, nous ne pouvons pas affirmer avec certitude qu'il s'en remettra à ce court-circuit mais finira par se faire mordre. Donc, pour moi, la réponse la plus sûre est:

Comme vous pouvez généralement vous fier CASEà l’évaluation gauche à droite et aux courts-circuits, comme indiqué dans la documentation, il n’est pas exact de dire que vous pouvez toujours le faire. Sur cette page, il existe deux cas de figure où ce n'est pas vrai et où aucun bogue n'a été corrigé dans aucune version publique de SQL Server.

EDIT est un autre cas (je dois cesser de le faire) où une CASEexpression ne s’évalue pas dans l’ordre que vous attendez, même si aucun agrégat n’est impliqué.


2
Et on dirait qu'il y a un autre problème avec CASE ça qui a été réglé tranquillement
Martin Smith

OMI cela ne prouve pas que l'évaluation de CASE expression n'est pas garantie car les valeurs agrégées sont calculées avant la sélection (de sorte qu'elles puissent être utilisées en mode interne).
Salman Un

1
@SalmanA Je ne sais pas quoi d'autre cela peut faire si ce n'est de prouver exactement que l'ordre d'évaluation dans une expression CASE n'est pas garanti. Nous obtenons une exception parce que l'agrégat est calculé en premier, même s'il s'agit d'une clause ELSE qui, si vous vous reportez à la documentation, ne devrait jamais être atteinte.
Aaron Bertrand

Les agrégats @AaronBertrand sont calculés avant l' instruction CASE (et ils doivent être à l'OMI). La documentation révisée indique exactement que l'erreur se produit avant l' évaluation de CASE.
Salman Un

@SalmanA Cela montre toujours au développeur occasionnel que l'expression CASE n'évalue pas l'ordre dans lequel elle a été écrite - les mécanismes sous-jacents ne sont pas pertinents si vous essayez simplement de comprendre pourquoi une erreur provenant d'une branche CASE ne devrait pas ' t ont été atteints. Avez-vous également des arguments contre tous les autres exemples de cette page?
Aaron Bertrand

37

Mon point de vue à ce sujet est que la documentation, il est assez clair que l' intention est que CASE devrait court-circuit. Comme Aaron le mentionne, il y a eu plusieurs cas (ha!) Où il a été démontré que cela n'était pas toujours vrai.

Jusqu'à présent, tous ces problèmes ont été reconnus comme des bogues et corrigés - mais pas nécessairement dans une version de SQL Server, vous pouvez acheter et mettre à jour aujourd'hui (le bogue de repliement constant n'a pas encore abouti à une mise à jour cumulative pour autant que je sache). Le plus récent bogue potentiel - signalé à l'origine par Itzik Ben-Gan - n'a pas encore été étudié (Aaron ou moi l'ajouterons à Connect prochainement).

En relation avec la question initiale, il existe d'autres problèmes avec CASE (et donc avec COALESCE) dans lesquels des fonctions ou des sous-requêtes ayant des effets secondaires sont utilisées. Considérer:

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

Le formulaire COALESCE renvoie souvent NULL, plus de détails à l' adresse https://connect.microsoft.com/SQLServer/feedback/details/546437/coalesce-subquery-1-may-return-null

Les problèmes démontrés avec les transformations de l'optimiseur et le suivi de l'expression commune signifient qu'il est impossible de garantir que CASE sera en court-circuit dans toutes les circonstances. Je peux concevoir des cas où il pourrait même ne pas être possible de prédire le comportement en inspectant la sortie du plan d'exposition publique, bien que je n'ai pas de repro pour cela aujourd'hui.

En résumé, je pense que vous pouvez être raisonnablement confiant que CASE court-circuitera en général (particulièrement si une personne assez qualifiée inspecte le plan d'exécution et que ce plan est "appliqué" avec un guide de planification ou des astuces), mais si vous en avez besoin une garantie absolue, vous devez écrire du SQL qui n'inclut pas du tout l'expression.

La situation n’est pas très satisfaisante, je suppose.


18

J'ai rencontré un autre cas où CASE/ COALESCEne court-circuite pas. La TVF suivante déclenchera une violation de clé PK si elle est transmise en 1tant que paramètre.

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

      RETURN
  END

Si appelé comme suit

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 comme

DECLARE @Number INT = 1

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

Les deux donnent le résultat

Violation de la contrainte PRIMARY KEY 'PK__F__3BD019A800551192'. Impossible d'insérer une clé en double dans l'objet 'dbo. @ T'. La valeur de la clé en double est (1).

montrant que la SELECT(ou au moins la population variable du tableau) est toujours exécutée et génère une erreur même si cette branche de la déclaration ne doit jamais être atteinte. Le plan pour la COALESCEversion est ci-dessous.

Plan

Cette réécriture de la requête semble éviter le problème

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)   

Qui donne plan

Plan2


8

Un autre exemple

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 requête

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)

Ne montre aucune lecture contre T2du tout.

La recherche de T2est sous un prédicat passant et l'opérateur n'est jamais exécuté. Mais

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)

Est -ce que montre que T2est lu. Même si aucune valeur de T2n'est jamais réellement nécessaire.

Bien sûr, cela n’est pas vraiment surprenant, mais j’ai pensé qu’il valait la peine d’ajouter au référentiel contre-exemples, ne serait-ce que parce que cela pose la question de savoir ce que court-circuit signifie même dans un langage déclaratif basé sur un ensemble.


7

Je voulais juste mentionner une stratégie que vous n'avez peut-être pas envisagée. Ce n'est peut-être pas un match, mais il est parfois utile. Voyez si cette modification vous donne de meilleures performances:

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
            )

Une autre façon de le faire pourrait être ceci (fondamentalement équivalent, mais vous permet d'accéder à plus de colonnes de l'autre requête si nécessaire):

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

Fondamentalement, il s’agit d’une technique de jonction «dure» de tables, mais qui inclut la condition à laquelle toutes les lignes doivent être JOINÉES. D'après mon expérience, cela a parfois vraiment aidé les plans d'exécution.


3

Non, ce ne serait pas. Il ne fonctionnera que quand c.FirstNameest NULL.

Cependant, vous devriez l'essayer vous-même. Expérience. Vous avez dit que votre sous-requête est longue. Référence. Tirez vos propres conclusions à ce sujet.

La réponse de @Aaron sur la sous-requête en cours d'exécution est plus complète.

Cependant, je pense toujours que vous devriez retravailler votre requête et l'utiliser LEFT JOIN. La plupart du temps, les sous-requêtes peuvent être supprimées en retravaillant votre requête pour utiliser LEFT JOINs.

Le problème lié à l'utilisation de sous-requêtes est que votre instruction globale s'exécutera plus lentement car la sous-requête est exécutée pour chaque ligne du jeu de résultats de la requête principale.


@Adrian, ce n'est toujours pas correct. Examinez le plan d'exécution et vous verrez que les sous-requêtes sont souvent converties assez intelligemment en JOIN. C'est une simple erreur de pensée que de supposer que la sous-requête entière doit être réexécutée pour chaque ligne, bien que cela puisse effectivement se produire si une jointure de boucle imbriquée avec une analyse est choisie.
ErikE

3

La norme actuelle indique que toutes les clauses WHEN (ainsi que la clause ELSE) doivent être analysées pour déterminer le type de données de l'expression dans son ensemble. Je devrais vraiment sortir certaines de mes anciennes notes pour déterminer comment une erreur est gérée. Mais pour commencer, 1/0 utilise des entiers, je suppose donc que c’est une erreur. C'est une erreur avec le type de données entier. Lorsque vous n'avez que des valeurs nulles dans la liste de fusion, il est un peu plus difficile de déterminer le type de données, et c'est un autre problème.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.