Section de réponse
Il existe différentes manières de réécrire cela en utilisant différentes constructions T-SQL. Nous allons examiner les avantages et les inconvénients et faire une comparaison globale ci-dessous.
Première place : utiliserOR
SELECT COUNT(*)
FROM dbo.Users AS u
WHERE u.Age < 18
OR u.Age IS NULL;
Utiliser OR
nous donne un plan de recherche plus efficace, qui lit le nombre exact de lignes dont nous avons besoin, mais ajoute ce que le monde technique appelle a whole mess of malarkey
au plan de requête.
Notez également que Seek est exécuté deux fois ici, ce qui devrait être plus évident pour l'opérateur graphique:
Table 'Users'. Scan count 2, logical reads 8233, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
SQL Server Execution Times:
CPU time = 469 ms, elapsed time = 473 ms.
Deuxième étape : utiliser les tables dérivées avec UNION ALL
Notre requête peut également être réécrit comme ceci
SELECT SUM(Records)
FROM
(
SELECT COUNT(Id)
FROM dbo.Users AS u
WHERE u.Age < 18
UNION ALL
SELECT COUNT(Id)
FROM dbo.Users AS u
WHERE u.Age IS NULL
) x (Records);
Cela donne le même type de plan, avec beaucoup moins de malkey, et un degré plus apparent d'honnêteté quant au nombre de fois où l'indice a été recherché (recherché?).
Il effectue la même quantité de lectures (8233) que la OR
requête, mais économise environ 100 ms de temps CPU.
CPU time = 313 ms, elapsed time = 315 ms.
Cependant, vous devez être très prudent ici, car si ce plan tente de passer en parallèle, les deux COUNT
opérations distinctes seront sérialisées, car elles sont considérées chacune comme un agrégat scalaire global. Si nous imposons un plan parallèle à l’aide de Trace Flag 8649, le problème devient évident.
SELECT SUM(Records)
FROM
(
SELECT COUNT(Id)
FROM dbo.Users AS u
WHERE u.Age < 18
UNION ALL
SELECT COUNT(Id)
FROM dbo.Users AS u
WHERE u.Age IS NULL
) x (Records)
OPTION(QUERYTRACEON 8649);
Ceci peut être évité en modifiant légèrement notre requête.
SELECT SUM(Records)
FROM
(
SELECT 1
FROM dbo.Users AS u
WHERE u.Age < 18
UNION ALL
SELECT 1
FROM dbo.Users AS u
WHERE u.Age IS NULL
) x (Records)
OPTION(QUERYTRACEON 8649);
À présent, les deux nœuds effectuant une recherche sont entièrement parallélisés jusqu'à ce que nous atteignions l'opérateur de concaténation.
Pour ce que cela vaut, la version entièrement parallèle présente de bons avantages. Au prix d'environ 100 lectures supplémentaires et d'environ 90 ms de temps CPU supplémentaire, le temps écoulé est réduit à 93 ms.
Table 'Users'. Scan count 12, logical reads 8317, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
SQL Server Execution Times:
CPU time = 500 ms, elapsed time = 93 ms.
Qu'en est-il de CROSS APPLY?
Aucune réponse n'est complète sans la magie de CROSS APPLY
!
Malheureusement, nous rencontrons plus de problèmes avec COUNT
.
SELECT SUM(Records)
FROM dbo.Users AS u
CROSS APPLY
(
SELECT COUNT(Id)
FROM dbo.Users AS u2
WHERE u2.Id = u.Id
AND u2.Age < 18
UNION ALL
SELECT COUNT(Id)
FROM dbo.Users AS u2
WHERE u2.Id = u.Id
AND u2.Age IS NULL
) x (Records);
Ce plan est horrible. C'est le genre de plan que vous utilisez lorsque vous vous présentez pour la dernière fois à la Saint-Patrick. Bien que parallèle, il numérise pour l’instant le PK / CX. Ew. Le plan a un coût de 2198 dollars d'interrogation.
Table 'Users'. Scan count 7, logical reads 31676233, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Worktable'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
SQL Server Execution Times:
CPU time = 29532 ms, elapsed time = 5828 ms.
Ce qui est un choix étrange, car si nous le forçons à utiliser l’index non clusterisé, le coût baisse de manière assez significative à 1798 dollars de requêtes.
SELECT SUM(Records)
FROM dbo.Users AS u
CROSS APPLY
(
SELECT COUNT(Id)
FROM dbo.Users AS u2 WITH (INDEX(ix_Id_Age))
WHERE u2.Id = u.Id
AND u2.Age < 18
UNION ALL
SELECT COUNT(Id)
FROM dbo.Users AS u2 WITH (INDEX(ix_Id_Age))
WHERE u2.Id = u.Id
AND u2.Age IS NULL
) x (Records);
Hey, cherche! Vérifiez-vous là-bas. Notez également qu'avec la magie de CROSS APPLY
, nous n'avons pas besoin de faire quelque chose de maladroit pour avoir un plan presque entièrement parallèle.
Table 'Users'. Scan count 5277838, logical reads 31685303, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Worktable'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
SQL Server Execution Times:
CPU time = 27625 ms, elapsed time = 4909 ms.
L'application croisée finit par se porter mieux sans les COUNT
éléments qu'elle contient .
SELECT SUM(Records)
FROM dbo.Users AS u
CROSS APPLY
(
SELECT 1
FROM dbo.Users AS u2
WHERE u2.Id = u.Id
AND u2.Age < 18
UNION ALL
SELECT 1
FROM dbo.Users AS u2
WHERE u2.Id = u.Id
AND u2.Age IS NULL
) x (Records);
Le plan a l'air bien, mais les lectures et le processeur ne constituent pas une amélioration.
Table 'Users'. Scan count 20, logical reads 17564, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Workfile'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Worktable'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
SQL Server Execution Times:
CPU time = 4844 ms, elapsed time = 863 ms.
La réécriture de la croix s’applique pour devenir une jointure dérivée et donne exactement le même résultat. Je ne vais pas publier à nouveau le plan de requête et les informations statistiques - elles n'ont pas vraiment changé.
SELECT COUNT(u.Id)
FROM dbo.Users AS u
JOIN
(
SELECT u.Id
FROM dbo.Users AS u
WHERE u.Age < 18
UNION ALL
SELECT u.Id
FROM dbo.Users AS u
WHERE u.Age IS NULL
) x ON x.Id = u.Id;
Algèbre relationnelle : pour être rigoureux et empêcher Joe Celko de hanter mes rêves, nous devons au moins essayer des techniques relationnelles étranges. Ici ne va rien!
Une tentative avec INTERSECT
SELECT COUNT(*)
FROM dbo.Users AS u
WHERE NOT EXISTS ( SELECT u.Age WHERE u.Age >= 18
INTERSECT
SELECT u.Age WHERE u.Age IS NOT NULL );
Table 'Users'. Scan count 1, logical reads 9157, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
SQL Server Execution Times:
CPU time = 1094 ms, elapsed time = 1090 ms.
Et voici une tentative avec EXCEPT
SELECT COUNT(*)
FROM dbo.Users AS u
WHERE NOT EXISTS ( SELECT u.Age WHERE u.Age >= 18
EXCEPT
SELECT u.Age WHERE u.Age IS NULL);
Table 'Users'. Scan count 7, logical reads 9247, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
SQL Server Execution Times:
CPU time = 2126 ms, elapsed time = 376 ms.
Il y a peut-être d'autres façons d'écrire cela, mais je laisserai cela à ceux qui utilisent peut-être EXCEPT
et INTERSECT
plus souvent que moi.
Si vous avez vraiment besoin d'un chiffre
que j'utilise COUNT
dans mes requêtes comme un raccourci (lisez: je suis trop paresseux pour proposer des scénarios plus compliqués parfois). Si vous avez juste besoin d'un compte, vous pouvez utiliser une CASE
expression pour faire à peu près la même chose.
SELECT SUM(CASE WHEN u.Age < 18 THEN 1
WHEN u.Age IS NULL THEN 1
ELSE 0 END)
FROM dbo.Users AS u
SELECT SUM(CASE WHEN u.Age < 18 OR u.Age IS NULL THEN 1
ELSE 0 END)
FROM dbo.Users AS u
Ces deux systèmes ont le même plan, le même processeur et les mêmes caractéristiques de lecture.
Table 'Users'. Scan count 1, logical reads 9157, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
SQL Server Execution Times:
CPU time = 719 ms, elapsed time = 719 ms.
Le gagnant?
Dans mes tests, le plan parallèle forcé avec SUM sur une table dérivée donnait les meilleurs résultats. Et oui, beaucoup de ces requêtes auraient pu être aidées en ajoutant quelques index filtrés pour prendre en compte les deux prédicats, mais je voulais laisser quelques expériences à d'autres.
SELECT SUM(Records)
FROM
(
SELECT 1
FROM dbo.Users AS u
WHERE u.Age < 18
UNION ALL
SELECT 1
FROM dbo.Users AS u
WHERE u.Age IS NULL
) x (Records)
OPTION(QUERYTRACEON 8649);
Merci!
NOT EXISTS ( INTERSECT / EXCEPT )
requêtes peuvent fonctionner sans lesINTERSECT / EXCEPT
parties:WHERE NOT EXISTS ( SELECT u.Age WHERE u.Age >= 18 );
Une autre façon - qui utiliseEXCEPT
:SELECT COUNT(*) FROM (SELECT UserID FROM dbo.Users EXCEPT SELECT UserID FROM dbo.Users WHERE u.Age >= 18) AS u ;
(où ID utilisateur est le PK ou toute colonne unique non nulle).