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).SELECT result = (SELECT COUNT(*) FROM dbo.Users AS u WHERE u.Age < 18) + (SELECT COUNT(*) FROM dbo.Users AS u WHERE u.Age IS NULL) ;
Désolé si j'ai manqué les millions de versions que vous avez testées!UNION ALL
plans (CPU 360ms, 11k lectures).Je n'étais pas sur le point de restaurer une base de données de 110 Go pour une seule table, j'ai donc créé mes propres données . Les distributions par âge doivent correspondre à ce qui est sur Stack Overflow, mais il est évident que la table elle-même ne correspondra pas. Je ne pense pas que ce soit un problème car les requêtes vont de toute façon atteindre les index. Je teste sur un ordinateur à 4 processeurs avec SQL Server 2016 SP1. Une chose à noter est que pour les requêtes qui terminent rapidement, il est important de ne pas inclure le plan d'exécution réel. Cela peut ralentir un peu les choses.
J'ai commencé par examiner certaines des solutions de l'excellente réponse d'Erik. Pour celui-ci:
J'ai obtenu les résultats suivants de sys.dm_exec_sessions après 10 essais (la requête était naturellement parallèle pour moi):
La requête qui a mieux fonctionné pour Erik a en réalité moins bien fonctionné sur ma machine:
Résultats de 10 essais:
Je ne peux pas tout de suite expliquer pourquoi c'est si grave, mais on ne sait pas pourquoi nous voulons forcer presque tous les opérateurs du plan de requête à passer en parallèle. Dans le plan d'origine, nous avons une zone série qui trouve toutes les lignes avec
AGE < 18
. Il n'y a que quelques milliers de lignes. Sur ma machine, je reçois 9 lectures logiques pour cette partie de la requête et 9 ms de temps processeur signalé et de temps écoulé. Il existe également une zone série pour l'agrégat global pour les lignes avecAGE IS NULL
mais ne traite qu'une ligne par DOP. Sur ma machine, il n'y a que quatre rangées.Ce que je retiens, c'est qu'il est primordial d'optimiser la partie de la requête qui trouve des lignes avec un
NULL
carAge
car il y en a des millions. Je n'ai pas été capable de créer un index avec moins de pages couvrant les données qu'un simple index compressé sur la colonne. Je suppose qu’il existe une taille d’index minimale par ligne ou qu’une grande partie de l’espace index ne peut être évitée avec les astuces que j’ai essayées. Donc, si nous avons à peu près le même nombre de lectures logiques pour obtenir les données, le seul moyen de le rendre plus rapide consiste à rendre la requête plus parallèle, mais cela doit être fait différemment de la requête d'Erik utilisant TF. 8649. Dans la requête ci-dessus, nous avons un rapport de 3,62 entre le temps CPU et le temps écoulé, ce qui est plutôt bon. L'idéal serait un ratio de 4,0 sur ma machine.Un domaine d’amélioration possible consiste à répartir le travail plus uniformément entre les tâches. Dans la capture d'écran ci-dessous, nous pouvons voir qu'un de mes processeurs a décidé de faire une petite pause:
Le balayage d'index est l'un des rares opérateurs pouvant être implémentés en parallèle et nous ne pouvons rien faire sur la manière dont les lignes sont distribuées aux threads. Il y a aussi un élément de chance, mais assez régulièrement, j'ai vu un fil sous-travaillé. Une façon de contourner ce problème consiste à effectuer le parallélisme à la dure, en joignant la partie interne d'une boucle imbriquée. Tout ce qui se trouve sur la partie interne d'une boucle imbriquée sera implémenté en série, mais de nombreux threads en série peuvent être exécutés simultanément. Tant que nous obtenons une méthode de distribution parallèle favorable (telle que le round robin), nous pouvons contrôler exactement le nombre de lignes envoyées à chaque thread.
Je lance des requêtes avec DOP 4, je dois donc diviser les
NULL
lignes du tableau en quatre compartiments. Une façon de faire est de créer un groupe d’index sur des colonnes calculées:Je ne sais pas trop pourquoi quatre index distincts sont un peu plus rapides qu'un index, mais c'est l'un de ceux que j'ai trouvés lors de mes tests.
Pour obtenir un plan de boucle imbriqué parallèle, je vais utiliser l' indicateur de trace non documenté 8649 . Je vais également écrire le code un peu étrangement pour encourager l'optimiseur à ne pas traiter plus de lignes que nécessaire. Ci-dessous une implémentation qui semble bien fonctionner:
Les résultats de dix essais:
Avec cette requête, nous avons un ratio temps processeur / temps écoulé de 3,85! Nous avons rasé 17 ms du temps d'exécution et il n'a fallu que 4 colonnes et index calculés pour le faire! Chaque thread traite globalement très près du même nombre de lignes car chaque index a très peu le même nombre de lignes et chaque thread analyse seulement un index:
Sur une note finale, nous pouvons également appuyer sur le bouton easy et ajouter un CCI non clusterisé à la
Age
colonne:La requête suivante se termine en 3 ms sur ma machine:
Cela va être difficile à battre.
la source
Bien que je ne dispose pas d'une copie locale de la base de données Stack Overflow, j'ai pu essayer quelques requêtes. Je pensais obtenir un nombre d'utilisateurs à partir d'une vue de catalogue système (au lieu d'obtenir directement un nombre de lignes de la table sous-jacente). Ensuite, obtenez un nombre de lignes qui correspondent (ou peut-être pas) aux critères d'Erik, et faites des calculs simples.
J'ai utilisé l' explorateur de données Stack Exchange (avec
SET STATISTICS TIME ON;
etSET STATISTICS IO ON;
) pour tester les requêtes. Pour un point de référence, voici quelques requêtes et les statistiques CPU / IO:QUERY 1
QUERY 2
QUERY 3
1ère tentative
C'était plus lent que toutes les requêtes d'Erik que j'ai énumérées ici ... du moins en termes de temps écoulé.
2e tentative
Ici, j'ai opté pour une variable pour stocker le nombre total d'utilisateurs (au lieu d'une sous-requête). Le nombre de numérisations est passé de 1 à 17 par rapport à la première tentative. Les lectures logiques sont restées les mêmes. Cependant, le temps écoulé a considérablement diminué.
Autres notes: DBCC TRACEON n'est pas autorisé sur Stack Exchange Data Explorer, comme indiqué ci-dessous:
la source
SELECT SUM(p.Rows) - (SELECT COUNT(*) FROM dbo.Users AS u WHERE u.Age >= 18 ) FROM sys.partitions p WHERE p.index_id < 2 AND p.object_id = OBJECT_ID('dbo.Users')
Utiliser des variables?
Par le commentaire peut ignorer les variables
la source
SELECT (select count(*) from table_1 where bb <= 1) + (select count(*) from table_1 where bb is null);
Bien utiliser
SET ANSI_NULLS OFF;
C’est quelque chose qui m’arrive à l’esprit. Je viens de l’exécuter dans https://data.stackexchange.com.
Mais pas aussi efficace que @blitz_erik bien
la source
Une solution triviale consiste à calculer le nombre (*) - nombre (âge> = 18):
Ou:
Résultats ici
la source