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

Le plan pour les données d'Ypercube est

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

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

TOP 3peut-êtreTOP 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 unhash match (flow distinct)opérateur plutôt quehash match (aggregate)oudistinct sort