Comment vérifier efficacement EXISTS sur plusieurs colonnes?

26

C'est un problème auquel je me heurte périodiquement et je n'ai pas encore trouvé de bonne solution.

Supposons la structure de table suivante

CREATE TABLE T
(
A INT PRIMARY KEY,
B CHAR(1000) NULL,
C CHAR(1000) NULL
)

et l'exigence consiste à déterminer si l'une des colonnes annulables Bou Ccontient réellement des NULLvaleurs (et si oui, laquelle (s)).

Supposons également que le tableau contient des millions de lignes (et qu'aucune statistique de colonne ne soit disponible qui pourrait être consultée car je suis intéressé par une solution plus générique pour cette classe de requêtes).

Je peux penser à quelques façons d'aborder cela, mais toutes ont des faiblesses.

Deux EXISTSdéclarations distinctes . Cela aurait l'avantage de permettre aux requêtes d'arrêter l'analyse dès qu'une détection NULLest trouvée. Mais si les deux colonnes ne contiennent en fait aucun NULLs, deux analyses complètes en résulteront.

Requête d'agrégat unique

SELECT 
    MAX(CASE WHEN B IS NULL THEN 1 ELSE 0 END) AS B,
    MAX(CASE WHEN C IS NULL THEN 1 ELSE 0 END) AS C
FROM T

Cela pourrait traiter les deux colonnes en même temps, donc avoir le pire des cas d'une analyse complète. L'inconvénient est que même s'il rencontre un NULLdans les deux colonnes très tôt dans la requête, il finira toujours par analyser le reste du tableau.

Variables utilisateur

Je peux penser à une troisième façon de faire

BEGIN TRY
DECLARE @B INT, @C INT, @D INT

SELECT 
    @B = CASE WHEN B IS NULL THEN 1 ELSE @B END,
    @C = CASE WHEN C IS NULL THEN 1 ELSE @C END,
    /*Divide by zero error if both @B and @C are 1.
    Might happen next row as no guarantee of order of
    assignments*/
    @D = 1 / (2 - (@B + @C))
FROM T  
OPTION (MAXDOP 1)       
END TRY
BEGIN CATCH
IF ERROR_NUMBER() = 8134 /*Divide by zero*/
    BEGIN
    SELECT 'B,C both contain NULLs'
    RETURN;
    END
ELSE
    RETURN;
END CATCH

SELECT ISNULL(@B,0),
       ISNULL(@C,0)

mais cela ne convient pas au code de production car le comportement correct pour une requête de concaténation agrégée n'est pas défini. et terminer l'analyse en lançant une erreur est de toute façon une solution horrible.

Existe-t-il une autre option qui combine les points forts des approches ci-dessus?

modifier

Juste pour mettre à jour cela avec les résultats que j'obtiens en termes de lectures pour les réponses soumises jusqu'à présent (en utilisant les données de test de @ ypercube)

+----------+------------+------+---------+----------+----------------------+----------+------------------+
|          | 2 * EXISTS | CASE | Kejser  |  Kejser  |        Kejser        | ypercube |       8kb        |
+----------+------------+------+---------+----------+----------------------+----------+------------------+
|          |            |      |         | MAXDOP 1 | HASH GROUP, MAXDOP 1 |          |                  |
| No Nulls |      15208 | 7604 |    8343 | 7604     | 7604                 |    15208 | 8346 (8343+3)    |
| One Null |       7613 | 7604 |    8343 | 7604     | 7604                 |     7620 | 7630 (25+7602+3) |
| Two Null |         23 | 7604 |    8343 | 7604     | 7604                 |       30 | 30 (18+12)       |
+----------+------------+------+---------+----------+----------------------+----------+------------------+

Pour @ la réponse de Thomas je l' ai changé TOP 3pour TOP 2permettre éventuellement à sortir plus tôt. J'ai obtenu un plan parallèle par défaut pour cette réponse, j'ai donc également essayé avec un MAXDOP 1indice afin de rendre le nombre de lectures plus comparable aux autres plans. J'ai été quelque peu surpris par les résultats car lors de mon test précédent, j'avais vu cette requête court-circuiter sans lire la table entière.

Le plan de mes données de test que les courts-circuits est ci-dessous

Des courts-circuits

Le plan pour les données d'Ypercube est

Pas de court-circuit

Il ajoute donc un opérateur de tri bloquant au plan. J'ai également essayé avec l' HASH GROUPindice, mais cela finit toujours par lire toutes les lignes

Pas de court-circuit

La clé semble donc être d'obtenir un hash match (flow distinct)opérateur pour permettre à ce plan de court-circuiter car les autres alternatives bloqueront et consommeront toutes les lignes de toute façon. Je ne pense pas qu'il y ait un indice pour forcer cela spécifiquement mais apparemment "en général, l'optimiseur choisit un flux distinct où il détermine que moins de lignes de sortie sont nécessaires qu'il y a de valeurs distinctes dans l'ensemble d'entrée." .

Les données de @ ypercube ont seulement 1 ligne dans chaque colonne avec des NULLvaleurs (cardinalité de table = 30300) et les lignes estimées entrant et sortant de l'opérateur sont les deux 1. En rendant le prédicat un peu plus opaque pour l'optimiseur, il a généré un plan avec l'opérateur Flow Distinct.

SELECT TOP 2 *
FROM (SELECT DISTINCT 
        CASE WHEN b IS NULL THEN NULL ELSE 'foo' END AS b
      , CASE WHEN c IS NULL THEN NULL ELSE 'bar' END AS c
  FROM test T 
  WHERE LEFT(b,1) + LEFT(c,1) IS NULL
) AS DT 

Modifier 2

Un dernier ajustement qui m'est venu à l'esprit est que la requête ci-dessus pourrait toujours finir par traiter plus de lignes que nécessaire dans le cas où la première ligne qu'elle rencontre avec un NULLa des valeurs NULL dans les deux colonnes Bet C. Il continuera à analyser plutôt qu'à quitter immédiatement. Une façon d'éviter cela serait de débloquer les lignes lors de leur numérisation. Donc, mon dernier amendement à la réponse de Thomas Kejser est ci-dessous

SELECT DISTINCT TOP 2 NullExists
FROM test T 
CROSS APPLY (VALUES(CASE WHEN b IS NULL THEN 'b' END),
                   (CASE WHEN c IS NULL THEN 'c' END)) V(NullExists)
WHERE NullExists IS NOT NULL

Il serait probablement préférable que le prédicat soit, WHERE (b IS NULL OR c IS NULL) AND NullExists IS NOT NULLmais contre les données de test précédentes, on ne me donne pas un plan avec un flux distinct, alors que celui- NullExists IS NOT NULLci le fait (plan ci-dessous).

Non pivoté

Martin Smith
la source

Réponses:

20

Que diriez-vous:

SELECT TOP 3 *
FROM (SELECT DISTINCT 
        CASE WHEN B IS NULL THEN NULL ELSE 'foo' END AS B
        , CASE WHEN C IS NULL THEN NULL ELSE 'bar' END AS C
  FROM T 
  WHERE 
    (B IS NULL AND C IS NOT NULL) 
    OR (B IS NOT NULL AND C IS NULL) 
    OR (B IS NULL AND C IS NULL)
) AS DT
Thomas Kejser
la source
J'aime cette approche. Il y a quelques problèmes possibles que j'aborde dans les modifications de ma question. Comme l' écrit TOP 3peut-être TOP 2comme actuellement il va scanner jusqu'à ce qu'il trouve un de chacun des éléments suivants (NOT_NULL,NULL), (NULL,NOT_NULL), (NULL,NULL). N'importe quel 2 de ces 3 serait suffisant - et s'il trouve le (NULL,NULL)premier, le second ne serait pas nécessaire non plus. De plus, afin de court-circuiter le plan devrait implémenter le distinct via un hash match (flow distinct)opérateur plutôt que hash match (aggregate)oudistinct sort
Martin Smith
6

Si je comprends bien la question, vous voulez savoir si une valeur null existe dans l'une des valeurs des colonnes, par opposition au retour réel des lignes dans lesquelles B ou C est nul. Si tel est le cas, alors pourquoi pas:

Select Top 1 'B as nulls' As Col
From T
Where T.B Is Null
Union All
Select Top 1 'C as nulls'
From T
Where T.C Is Null

Sur mon banc de test avec SQL 2008 R2 et un million de lignes, j'ai obtenu les résultats suivants en ms à partir de l'onglet Statistiques client:

Kejser                          2907,2875,2829,3576,3103
ypercube                        2454,1738,1743,1765,2305
OP single aggregate solution    (stopped after 120,000 ms) Wouldn't even finish
My solution                     1619,1564,1665,1675,1674

Si vous ajoutez l'indice nolock, les résultats sont encore plus rapides:

Select Top 1 'B as nulls' As Col
From T With(Nolock)
Where T.B Is Null
Union All
Select Top 1 'C as nulls'
From T With(Nolock)
Where T.C Is Null

My solution (with nolock)       42,70,94,138,120

Pour référence, j'ai utilisé le générateur SQL de Red-gate pour générer les données. Sur mon million de lignes, 9 886 lignes avaient une valeur B nulle et 10 019 avaient une valeur C nulle.

Dans cette série de tests, chaque ligne de la colonne B a une valeur:

Kejser                          245200  Scan count 1, logical reads 367259, physical reads 858, read-ahead reads 367278
                                250540  Scan count 1, logical reads 367259, physical reads 860, read-ahead reads 367280

ypercube(1)                     249137  Scan count 2, logical reads 367276, physical reads 850, read-ahead reads 367278
                                248276  Scan count 2, logical reads 367276, physical reads 869, read-ahead reads 368765

My solution                     250348  Scan count 2, logical reads 367276, physical reads 858, read-ahead reads 367278
                                250327  Scan count 2, logical reads 367276, physical reads 854, read-ahead reads 367278

Avant chaque test (les deux séries), j'ai couru CHECKPOINTet DBCC DROPCLEANBUFFERS.

Voici les résultats lorsqu'il n'y a pas de null dans le tableau. Notez que les 2 solutions existantes fournies par ypercube sont quasiment identiques aux miennes en termes de temps de lecture et d'exécution. Je (nous) pensons que cela est dû aux avantages de l'édition Entreprise / Développeur ayant recours à l'analyse avancée . Si vous utilisiez uniquement l'édition Standard ou inférieure, la solution de Kejser pourrait très bien être la solution la plus rapide.

Kejser                          248875  Scan count 1, logical reads 367259, physical reads 860, read-ahead reads 367290

ypercube(1)                     243349  Scan count 2, logical reads 367265, physical reads 851, read-ahead reads 367278
                                242729  Scan count 2, logical reads 367265, physical reads 858, read-ahead reads 367276
                                242531  Scan count 2, logical reads 367265, physical reads 855, read-ahead reads 367278

My solution                     243094  Scan count 2, logical reads 367265, physical reads 857, read-ahead reads 367278
                                243444  Scan count 2, logical reads 367265, physical reads 857, read-ahead reads 367278
Thomas
la source
4

Les IFdéclarations sont-elles autorisées?

Cela devrait vous permettre de confirmer l'existence de B ou C en un seul passage dans le tableau:

DECLARE 
  @A INT, 
  @B CHAR(10), 
  @C CHAR(10)

SET @B = 'X'
SET @C = 'X'

SELECT TOP 1 
  @A = A, 
  @B = B, 
  @C = C
FROM T 
WHERE B IS NULL OR C IS NULL 

IF @@ROWCOUNT = 0 
BEGIN 
  SELECT 'No nulls'
  RETURN
END

IF @B IS NULL AND @C IS NULL
BEGIN
  SELECT 'Both null'
  RETURN
END 

IF @B IS NULL 
BEGIN
  SELECT TOP 1 
    @C = C
  FROM T
  WHERE A > @A
  AND C IS NULL

  IF @B IS NULL AND @C IS NULL 
  BEGIN
    SELECT 'Both null'
    RETURN
  END
  ELSE
  BEGIN
    SELECT 'B is null'
    RETURN
  END
END

IF @C IS NULL 
BEGIN
  SELECT TOP 1 
    @B = B
  FROM T 
  WHERE A > @A
  AND B IS NULL

  IF @C IS NULL AND @B IS NULL
  BEGIN
    SELECT 'Both null'
    RETURN
  END
  ELSE
  BEGIN
    SELECT 'C is null'
    RETURN
  END
END      
8kb
la source
4

Testé en SQL-Fiddle dans les versions: 2008 r2 et 2012 avec 30K lignes.

  • La EXISTSrequête montre un énorme avantage en termes d'efficacité lorsqu'elle trouve Nulls tôt - ce qui est attendu.
  • Je reçois de meilleures performances avec la EXISTSrequête - dans tous les cas en 2012, ce que je ne peux pas expliquer.
  • En 2008R2, lorsqu'il n'y a pas de Nulls, c'est plus lent que les 2 autres requêtes. Plus il trouve tôt les valeurs Null, plus il obtient rapidement et lorsque les deux colonnes ont des valeurs NULL plus tôt, c'est beaucoup plus rapide que les 2 autres requêtes.
  • La requête de Thomas Kejser semble fonctionner légèrement mais constamment mieux en 2012 et pire en 2008R2, par rapport à la CASErequête de Martin .
  • La version 2012 semble avoir de bien meilleures performances. Cela peut être lié aux paramètres des serveurs SQL-Fiddle et non seulement aux améliorations de l'optimiseur.

Requêtes et horaires. Horaires quand c'est fait:

  • 1er sans aucun nul
  • 2e avec colonne Bayant un NULLà un petit id.
  • 3ème avec les deux colonnes ayant NULLchacune une à petits identifiants.

C'est parti (il y a un problème avec les plans, je vais réessayer plus tard. Suivez les liens pour l'instant):


Requête avec 2 sous-requêtes EXISTS

SELECT 
      CASE WHEN EXISTS (SELECT * FROM test WHERE b IS NULL)
             THEN 1 ELSE 0 
      END AS B,
      CASE WHEN EXISTS (SELECT * FROM test WHERE c IS NULL)
             THEN 1 ELSE 0 
      END AS C ;

-------------------------------------
Times in ms (2008R2): 1344 - 596 -  1  
Times in ms   (2012):   26 -  14 -  2

Requête d'agrégat unique de Martin Smith

SELECT 
    MAX(CASE WHEN b IS NULL THEN 1 ELSE 0 END) AS B,
    MAX(CASE WHEN c IS NULL THEN 1 ELSE 0 END) AS C
FROM test ;

--------------------------------------
Times in ms (2008R2):  558 - 553 - 516  
Times in ms   (2012):   37 -  35 -  36

Requête de Thomas Kejser

SELECT TOP 3 *
FROM (SELECT DISTINCT 
        CASE WHEN B IS NULL THEN NULL ELSE 'foo' END AS b
      , CASE WHEN C IS NULL THEN NULL ELSE 'bar' END AS c
  FROM test T 
  WHERE 
    (B IS NULL AND C IS NOT NULL) 
    OR (B IS NOT NULL AND C IS NULL) 
    OR (B IS NULL AND C IS NULL)
) AS DT ;

--------------------------------------
Times in ms (2008R2):  859 - 705 - 668  
Times in ms   (2012):   24 -  19 -  18

Ma suggestion (1)

WITH tmp1 AS
  ( SELECT TOP (1) 
        id, b, c
    FROM test
    WHERE b IS NULL OR c IS NULL
    ORDER BY id 
  ) 

  SELECT 
      tmp1.*, 
      NULL AS id2, NULL AS b2, NULL AS c2
  FROM tmp1
UNION ALL
  SELECT *
  FROM
    ( SELECT TOP (1)
          tmp1.id, tmp1.b, tmp1.c,
          test.id AS id2, test.b AS b2, test.c AS c2 
      FROM test
        CROSS JOIN tmp1
      WHERE test.id >= tmp1.id
        AND ( test.b IS NULL AND tmp1.c IS NULL
           OR tmp1.b IS NULL AND test.c IS NULL
            )
      ORDER BY test.id
    ) AS x ;

--------------------------------------
Times in ms (2008R2): 1089 - 572 -  16   
Times in ms   (2012):   28 -  15 -   1

Il a besoin d'un peu de polissage sur la sortie mais l'efficacité est similaire à la EXISTSrequête. Je pensais que ce serait mieux quand il n'y a pas de null, mais les tests montrent que ce n'est pas le cas.


Suggestion (2)

Essayer de simplifier la logique:

CREATE TABLE tmp
( id INT
, b CHAR(1000)
, c CHAR(1000)
) ;

DELETE  FROM tmp ;

INSERT INTO tmp 
    SELECT TOP (1) 
        id, b, c
    FROM test
    WHERE b IS NULL OR c IS NULL
    ORDER BY id  ; 

INSERT INTO tmp 
    SELECT TOP (1)
        test.id, test.b, test.c 
      FROM test
        JOIN tmp 
          ON test.id >= tmp.id
      WHERE ( test.b IS NULL AND tmp.c IS NULL
           OR tmp.b IS NULL AND test.c IS NULL
            )
      ORDER BY test.id ;

SELECT *
FROM tmp ;

Il semble mieux fonctionner en 2008R2 que la suggestion précédente mais pire en 2012 (peut-être que le 2e INSERTpeut être réécrit en utilisant IF, comme la réponse de @ 8kb):

------------------------------------------
Times in ms (2008R2): 416+6 - 1+127 -  1+1   
Times in ms   (2012):  14+1 - 0+27  -  0+29
ypercubeᵀᴹ
la source
0

Lorsque vous utilisez EXISTS, SQL Server sait que vous effectuez un contrôle d'existence. Lorsqu'il trouve la première valeur correspondante, il renvoie VRAI et cesse de chercher.

lorsque vous concaténerez 2 colonnes et si aucune est nulle, le résultat sera nul

par exemple

null + 'a' = null

alors vérifiez ce code

IF EXISTS (SELECT 1 FROM T WHERE B+C is null)
SELECT Top 1 ISNULL(B,'B ') + ISNULL(C,'C') as [Nullcolumn] FROM T WHERE B+C is null
AmmarR
la source
-3

Que diriez-vous:

select 
    exists(T.B is null) as 'B is null',
    exists(T.C is null) as 'C is null'
from T;

Si cela fonctionne (je ne l'ai pas testé), cela produirait un tableau à une ligne avec 2 colonnes, chacune étant VRAIE ou FAUX. Je n'ai pas testé l'efficacité.

David Horowitz
la source
2
Même si cela est valable dans tout autre SGBD, je doute qu'il ait la bonne sémantique. En supposant que cela T.B is nullsoit alors traité comme un résultat booléen EXISTS(SELECT true)et EXISTS(SELECT false)que les deux renverraient true. Cet exemple MySQL indique que les deux colonnes contiennent NULL alors qu'aucune ne le fait
Martin Smith