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 Nullable B
ou C
contient réellement des valeurs NULL
(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 instructions EXISTS
distinctes. Cela aurait l'avantage de permettre aux requêtes d'arrêter l'analyse dès qu'un NULL
est trouvé. Mais si les deux colonnes ne contiennent en fait aucun NULL
, 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 très tôt un NULL
dans les deux colonnes, la requête finira toujours par analyser le reste du tableau.
Variables utilisateur
Je peut penser à une troisième façon de procéder
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, j'ai changé TOP 3
En TOP 2
Pour lui permettre potentiellement de se terminer plus tôt. J'ai obtenu un plan parallèle par défaut pour cette réponse, j'ai donc également essayé avec un indice MAXDOP 1
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'indication HASH GROUP
Mais cela finit toujours par lire toutes les lignes
Donc, la clé semble être d'obtenir un opérateur hash match (flow distinct)
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 valeurs NULL
(cardinalité de la table = 30300) et les lignes estimées entrant et sortant de l'opérateur sont toutes 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
Édition 2
Un dernier tweak 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 NULL
a des NULL dans les deux colonnes B
et 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 à 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 NULL
mais par rapport aux données de test précédentes, on ne me donne pas de plan avec un Flow Distinct, alors que celui de NullExists IS NOT NULL
Le fait (plan ci-dessous ).
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
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 depuis 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 ensembles), j'ai exécuté CHECKPOINT
et 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 utilisant 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
Testé dans SQL-Fiddle dans les versions: 2008 r2 et 2012 avec 30K lignes.
EXISTS
montre un énorme avantage en termes d'efficacité lorsqu'elle trouve Nulls tôt - ce qui est attendu.EXISTS
- dans tous les cas en 2012, ce que je ne peux expliquer.CASE
de Martin.Requêtes et horaires. Horaires quand c'est fait:
B
ayant un NULL
à un petit id
.NULL
chacune avec de 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 requête EXISTS
. 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 2ème INSERT
peut ê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
Les instructions IF
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
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érez 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