web-dev-qa-db-fra.com

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

J'utilise une fonction T-SQL COALESCE où le premier argument ne sera pas nul environ 95% des fois où il sera exécuté. Si le premier argument est NULL, le deuxième 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écuterait-il toujours la sous-requête?

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

102
Curt

Non . 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 levée pour la division par zéro.

D'après la documentation MSDN , cela est lié à la façon dont COALESCE est vu par l'interpréteur - c'est juste un moyen facile d'écrire une instruction CASE.

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 y a quelques exceptions lors de la comparaison avec des variables scalaires et des agrégations comme le montre Aaron Bertrand dans une autre réponse ici (et cela s'appliquerait à la fois à CASE et COALESCE):

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

générera une division par zéro erreur.

Cela devrait être considéré comme un bogue et, en règle générale, COALESCE analysera de gauche à droite.

96
JNK

Qu'en est-il de celui-ci - tel que rapporté par Itzik Ben-Gan, qui était raconté à ce sujet par Jaime Lafargue ?

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 CASE ne garantit pas toujours garantit une évaluation/un court-circuit de gauche à droite. J'ai signalé le bogue ici et il a été fermé "par conception". Paul White a ensuite déposé cet élément Connect , et il a été fermé comme fixe. Non pas parce qu'il 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 expression CASE. J'ai récemment blogué plus à ce sujet ici .

[~ # ~] éditez [~ # ~] juste un addendum, bien que je convienne que ce sont des cas Edge, que la plupart des time vous pouvez compter sur une évaluation et un court-circuitage de gauche à droite, et que ce sont des bogues qui contredisent la documentation et seront probablement éventuellement corrigés (ce n'est pas certain - voir la conversation de suivi sur article de blog de Bart Duncan pour voir pourquoi), je dois être en désaccord quand les gens disent que quelque chose est toujours vrai même s'il y a un seul cas Edge qui le réfute. Si Itzik et d'autres peuvent trouver des bogues solitaires comme celui-ci, cela rend au moins possible qu'il existe également d'autres bogues. Et comme nous ne connaissons pas le reste de la requête de l'OP, nous ne pouvons pas affirmer avec certitude qu'il s'appuiera sur ce court-circuit mais finira par en être mordu. Donc pour moi, la réponse la plus sûre est:

Bien que vous puissiez généralement compter sur CASE pour évaluer de gauche à droite et de court-circuit, comme décrit dans la documentation, il n'est pas exact de dire que vous pouvez toujours faire donc. Il y a deux cas illustrés sur cette page où ce n'est pas vrai et aucun bogue n'a été corrigé dans aucune version publique de SQL Server.

[~ # ~] modifier [~ # ~] voici un autre cas (je dois arrêter de faire ça) où un CASE expression n'évalue pas dans l'ordre que vous attendez, même si aucun agrégat n'est impliqué.

75
Aaron Bertrand

La documentation indique assez clairement que l'intention est de court-circuiter CASE. Comme Aaron mentionne , il y a eu plusieurs cas rapportés où cela s'est avéré pas toujours vrai. Jusqu'à présent, la plupart d'entre eux ont été reconnus comme bogues et corrigés.

Il y a d'autres problèmes avec CASE (et donc COALESCE) où des fonctions ou sous-requêtes à 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);

La forme COALESCE retourne souvent null, comme décrit dans n rapport de bogue par Hugo Kornelis.

Les problèmes démontrés avec les transformations de l'optimiseur et le suivi des expressions communes signifient qu'il est impossible de garantir que CASE court-circuitera en toutes circonstances.

Je pense que vous pouvez être raisonnablement sûr que CASE court-circuitera en général (en particulier si une personne raisonnablement qualifiée inspecte le plan d'exécution, et que le plan d'exécution est "appliqué" avec un guide de plan ou des conseils) mais si vous avez besoin d'une garantie absolue, vous devez écrire du SQL qui n'inclut pas du tout l'expression.

38
Paul White 9

J'ai rencontré un autre cas où CASE/COALESCE ne court-circuite pas. Le TVF suivant déclenchera une violation de PK s'il est réussi 1 comme 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 clé en double est (1).

montrant que le SELECT (ou au moins la population de variables de table) est toujours exécuté et génère une erreur même si cette branche de l'instruction ne doit jamais être atteinte. Le plan de la version COALESCE 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)   

Ce qui donne plan

Plan2

20
Martin Smith

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 T2 du tout.

La recherche de T2 est sous un prédicat de passage 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 T2 est lu. Même si aucune valeur de T2 est 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 des contre-exemples, ne serait-ce que parce que cela soulève la question de ce que signifie même le court-circuit dans un langage déclaratif basé sur un ensemble.

9
Martin Smith

Je voulais juste mentionner une stratégie que vous n'avez peut-être pas envisagée. Ce n'est peut-être pas un match ici, mais cela peut être utile parfois. 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 la suivante (essentiellement équivalente, 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 jointure "dure" des tables, mais incluant la condition sur le moment où des lignes doivent être jointes. D'après mon expérience, cela a parfois aidé les plans d'exécution.

7
ErikE

La norme actuelle stipule 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 juste à côté, 1/0 utilise des entiers, donc je suppose 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.

3
Joe Celko

Non, ce ne serait pas le cas. Il ne fonctionnerait que lorsque c.FirstName est NULL.

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

La réponse @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 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 avec 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.

2
Adriano Carneiro