De quoi il ne s'agit pas:
Ce n'est pas une question sur requêtes fourre-tout qui acceptent les entrées utilisateur ou utilisent des variables.
Il s'agit strictement de requêtes où ISNULL()
est utilisé dans la clause WHERE
pour remplacer les valeurs de NULL
par une valeur canari pour la comparaison avec un prédicat, et différentes façons de réécrire ces requêtes être SARGable dans SQL Server.
Pourquoi n'avez-vous pas de place là-bas?
Notre exemple de requête concerne une copie locale de la base de données Stack Overflow sur SQL Server 2016 et recherche les utilisateurs ayant un âge NULL
ou un âge <18.
SELECT COUNT(*)
FROM dbo.Users AS u
WHERE ISNULL(u.Age, 17) < 18;
Le plan de requête montre une analyse d'un index non cluster assez réfléchi.
L'opérateur d'analyse montre (grâce aux ajouts au plan d'exécution XML réel dans les versions plus récentes de SQL Server) que nous lisons chaque ligne puante.
Globalement, nous effectuons 9157 lectures et utilisons environ une demi-seconde de temps processeur:
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 = 485 ms, elapsed time = 483 ms.
La question: Quels sont les moyens de réécrire cette requête pour la rendre plus efficace, et peut-être même SARGable?
N'hésitez pas à proposer d'autres suggestions. Je ne pense pas que ma réponse soit nécessairement la réponse, et il y a suffisamment de gens intelligents pour trouver des alternatives qui pourraient être meilleures.
Si vous voulez jouer sur votre propre ordinateur, rendez-vous ici pour télécharger la base de données SO .
Merci!
Section des réponses
Il existe différentes façons de réécrire cela en utilisant différentes constructions T-SQL. Nous examinerons les avantages et les inconvénients et ferons une comparaison globale ci-dessous.
Première place : Utilisation de OR
SELECT COUNT(*)
FROM dbo.Users AS u
WHERE u.Age < 18
OR u.Age IS NULL;
L'utilisation de OR
nous donne un plan de recherche plus efficace, qui lit le nombre exact de lignes dont nous avons besoin, mais il ajoute ce que le monde technique appelle a whole mess of malarkey
au plan de requête.
Notez également que la recherche est exécutée deux fois ici, ce qui devrait vraiment ê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 place : Utilisation de tables dérivées avec UNION ALL
Notre requête peut également être réécrite 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 malarkey et un degré d'honnêteté plus apparent quant au nombre de fois où l'indice a été recherché (recherché?).
Il effectue le même nombre de lectures (8233) que la requête OR
, mais réduit d'environ 100 ms le temps processeur.
CPU time = 313 ms, elapsed time = 315 ms.
Cependant, vous devez être vraiment prudent ici, car si ce plan tente de se mettre en parallèle, les deux opérations COUNT
distinctes seront sérialisées, car elles sont chacune considérées comme globales agrégat scalaire. Si nous forçons un plan parallèle en utilisant 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);
Cela 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);
Maintenant, 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 ça vaut, la version entièrement parallèle a de bons avantages. Au prix d'environ 100 lectures supplémentaires et d'environ 90 ms de temps processeur supplémentaire, le temps écoulé se 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 avec lequel vous vous retrouvez lorsque vous vous présentez pour la Saint-Patrick. Bien que bien parallèle, pour une raison quelconque, il scanne le PK/CX. Ew. Le plan a un coût de 2198 dollars de requête.
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 chute assez sensiblement à 1798 dollars de requête.
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);
Hé, cherche! Vérifiez-vous là-bas. Notez également qu'avec la magie de CROSS APPLY
, nous n'avons rien à faire 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 réussit mieux sans les éléments COUNT
.
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 semble bon, mais les lectures et le processeur ne sont 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 être une jointure dérivée 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 vraiment pas 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 complet, et pour empêcher Joe Celko de hanter mes rêves, nous devons au moins essayer des trucs relationnels é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 peut y avoir d'autres façons de les écrire, mais je laisse cela aux personnes qui utilisent peut-être EXCEPT
et INTERSECT
plus souvent que moi.
Si vous avez vraiment besoin d'un décompte j'utilise COUNT
dans mes requêtes comme un raccourci (lire: je suis trop paresseux pour trouver des scénarios plus compliqués parfois). Si vous avez juste besoin d'un nombre, vous pouvez utiliser une expression CASE
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
Ils ont tous deux le même plan et ont 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 a donné les meilleurs résultats. Et oui, beaucoup de ces requêtes auraient pu être aidées en ajoutant quelques index filtrés pour tenir compte des deux prédicats, mais je voulais laisser une expérimentation à 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!
Je n'étais pas un jeu pour restaurer une base de données de 110 Go pour une seule table donc j'ai créé mes propres données . Les répartitions par âge devraient correspondre à ce qui se trouve sur Stack Overflow, mais la table elle-même ne correspondra évidemment pas. Je ne pense pas que ce soit trop un problème parce que les requêtes vont de toute façon atteindre les index. Je teste sur un ordinateur à 4 CPU avec SQL Server 2016 SP1. Une chose à noter est que pour les requêtes qui se 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 parcourir certaines des solutions de l'excellente réponse d'Erik. Pour celui-ci:
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);
J'ai obtenu les résultats suivants de sys.dm_exec_sessions sur 10 essais (la requête est naturellement allée en parallèle pour moi):
╔══════════╦════════════════════╦═══════════════╗
║ cpu_time ║ total_elapsed_time ║ logical_reads ║
╠══════════╬════════════════════╬═══════════════╣
║ 3532 ║ 975 ║ 60830 ║
╚══════════╩════════════════════╩═══════════════╝
La requête qui fonctionnait mieux pour Erik a en fait été moins bonne sur ma machine:
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);
Résultats de 10 essais:
╔══════════╦════════════════════╦═══════════════╗
║ cpu_time ║ total_elapsed_time ║ logical_reads ║
╠══════════╬════════════════════╬═══════════════╣
║ 5704 ║ 1636 ║ 60850 ║
╚══════════╩════════════════════╩═══════════════╝
Je ne suis pas immédiatement en mesure d'expliquer pourquoi c'est si mauvais, mais il n'est pas clair pourquoi nous voulons forcer presque tous les opérateurs du plan de requête à se mettre 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, j'obtiens 9 lectures logiques pour cette partie de la requête et 9 ms de temps CPU rapporté et de temps écoulé. Il existe également une zone série pour l'agrégat global pour les lignes avec AGE IS NULL
mais qui ne traite qu'une ligne par DOP. Sur ma machine, ce ne sont que quatre rangées.
Ma conclusion est qu'il est le plus important d'optimiser la partie de la requête qui trouve des lignes avec un NULL
pour Age
car il y a des millions de ces lignes. Je n'ai pas pu créer un index avec moins de pages qui couvraient les données qu'un simple page compressée sur la colonne. Je suppose qu'il y a une taille d'index minimum par ligne ou que beaucoup d'espace d'index ne peut pas être évité avec les astuces que j'ai essayées. Donc, si nous sommes coincés avec environ le même nombre de lectures logiques pour obtenir les données, la seule façon de les rendre plus rapides est de rendre la requête plus parallèle, mais cela doit être fait d'une manière différente de la requête d'Erik qui utilisait TF 8649. Dans la requête ci-dessus, nous avons un rapport de 3,62 pour le temps CPU sur le temps écoulé, ce qui est assez bon. L'idéal serait un rapport de 4,0 sur ma machine.
Un domaine d'amélioration possible consiste à répartir le travail de manière plus égale entre les threads. Dans la capture d'écran ci-dessous, nous pouvons voir que l'un de mes processeurs a décidé de faire une petite pause:
L'analyse 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 façon dont les lignes sont distribuées aux threads. Il y a aussi un élément de chance, mais j'ai toujours vu un fil sous-travaillé. Une façon de contourner ce problème est de faire le parallélisme à la dure: sur la partie intérieure d'une boucle imbriquée. Tout ce qui se trouve sur la partie interne d'une boucle imbriquée sera implémenté de manière série, mais de nombreux threads série peuvent s'exécuter simultanément. Tant que nous obtenons une méthode de distribution parallèle favorable (comme le round robin), nous pouvons contrôler exactement le nombre de lignes envoyées à chaque thread.
J'exécute des requêtes avec DOP 4, je dois donc diviser également les lignes NULL
du tableau en quatre compartiments. Une façon de le faire est de créer un tas d'index sur des colonnes calculées:
ALTER TABLE dbo.Users
ADD Compute_bucket_0 AS (CASE WHEN Age IS NULL AND Id % 4 = 0 THEN 1 ELSE NULL END),
Compute_bucket_1 AS (CASE WHEN Age IS NULL AND Id % 4 = 1 THEN 1 ELSE NULL END),
Compute_bucket_2 AS (CASE WHEN Age IS NULL AND Id % 4 = 2 THEN 1 ELSE NULL END),
Compute_bucket_3 AS (CASE WHEN Age IS NULL AND Id % 4 = 3 THEN 1 ELSE NULL END);
CREATE INDEX IX_Compute_bucket_0 ON dbo.Users (Compute_bucket_0) WITH (DATA_COMPRESSION = PAGE);
CREATE INDEX IX_Compute_bucket_1 ON dbo.Users (Compute_bucket_1) WITH (DATA_COMPRESSION = PAGE);
CREATE INDEX IX_Compute_bucket_2 ON dbo.Users (Compute_bucket_2) WITH (DATA_COMPRESSION = PAGE);
CREATE INDEX IX_Compute_bucket_3 ON dbo.Users (Compute_bucket_3) WITH (DATA_COMPRESSION = PAGE);
Je ne sais pas trop pourquoi quatre index distincts sont un peu plus rapides qu'un index, mais c'est celui que j'ai trouvé lors de mes tests.
Pour obtenir un plan de boucle imbriquée parallèle, je vais utiliser le non documenté indicateur de trace 8649 . Je vais également écrire le code un peu étrangement pour encourager l'optimiseur à ne pas traiter plus de lignes que nécessaire. Voici une implémentation qui semble bien fonctionner:
SELECT SUM(t.cnt) + (SELECT COUNT(*) FROM dbo.Users AS u WHERE u.Age < 18)
FROM
(VALUES (0), (1), (2), (3)) v(x)
CROSS APPLY
(
SELECT COUNT(*) cnt
FROM dbo.Users
WHERE Compute_bucket_0 = CASE WHEN v.x = 0 THEN 1 ELSE NULL END
UNION ALL
SELECT COUNT(*) cnt
FROM dbo.Users
WHERE Compute_bucket_1 = CASE WHEN v.x = 1 THEN 1 ELSE NULL END
UNION ALL
SELECT COUNT(*) cnt
FROM dbo.Users
WHERE Compute_bucket_2 = CASE WHEN v.x = 2 THEN 1 ELSE NULL END
UNION ALL
SELECT COUNT(*) cnt
FROM dbo.Users
WHERE Compute_bucket_3 = CASE WHEN v.x = 3 THEN 1 ELSE NULL END
) t
OPTION (QUERYTRACEON 8649);
Les résultats de dix essais:
╔══════════╦════════════════════╦═══════════════╗
║ cpu_time ║ total_elapsed_time ║ logical_reads ║
╠══════════╬════════════════════╬═══════════════╣
║ 3093 ║ 803 ║ 62008 ║
╚══════════╩════════════════════╩═══════════════╝
Avec cette requête, nous avons un rapport CPU/temps écoulé de 3,85! Nous avons réduit de 17 ms le temps d'exécution et il n'a fallu que 4 colonnes et index calculés pour le faire! Chaque thread traite très près du même nombre de lignes dans l'ensemble car chaque index a très près du même nombre de lignes et chaque thread ne scanne qu'un index:
Sur une note finale, nous pouvons également appuyer sur le bouton facile et ajouter un CCI non cluster à la colonne Age
:
CREATE NONCLUSTERED COLUMNSTORE INDEX X_NCCI ON dbo.Users (Age);
La requête suivante se termine en 3 ms sur ma machine:
SELECT COUNT(*)
FROM dbo.Users AS u
WHERE u.Age < 18 OR u.Age IS NULL;
Ça va être difficile à battre.
Bien que je n'ai pas de copie locale de la base de données Stack Overflow, j'ai pu essayer quelques requêtes. Ma pensée était d'obtenir un nombre d'utilisateurs à partir d'une vue de catalogue système (par opposition à 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é Stack Exchange Data Explorer (Avec SET STATISTICS TIME ON;
et SET 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
--Erik's query From initial question.
SELECT COUNT(*)
FROM dbo.Users AS u
WHERE ISNULL(u.Age, 17) < 18;
Temps d'exécution SQL Server: temps CPU = 0 ms, temps écoulé = 0 ms. (1 ligne (s) retournée)
Tableau "Utilisateurs". Nombre de balayages 17, lectures logiques 201567, lectures physiques 0, lectures anticipées 2740, lectures logiques lob 0, lectures physiques lob 0, lectures anticipées lob 0.
Temps d'exécution SQL Server: temps CPU = 1829 ms, temps écoulé = 296 ms.
REQUÊTE 2
--Erik's "OR" query.
SELECT COUNT(*)
FROM dbo.Users AS u
WHERE u.Age < 18
OR u.Age IS NULL;
Temps d'exécution SQL Server: temps CPU = 0 ms, temps écoulé = 0 ms. (1 ligne (s) retournée)
Tableau "Utilisateurs". Nombre de balayages 17, lectures logiques 201567, lectures physiques 0, lectures anticipées 0, lectures logiques 0, lob lectures physiques 0, lob lectures anticipées 0.
Temps d'exécution SQL Server: temps CPU = 2500 ms, temps écoulé = 147 ms.
REQUÊTE 3
--Erik's derived tables/UNION ALL query.
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);
Temps d'exécution SQL Server: temps CPU = 0 ms, temps écoulé = 0 ms. (1 ligne (s) retournée)
Tableau "Utilisateurs". Nombre de balayages 34, lectures logiques 403134, lectures physiques 0, lectures anticipées 0, lectures logiques 0, lob lectures physiques 0, lob lectures anticipées 0.
Temps d'exécution SQL Server: temps CPU = 3156 ms, temps écoulé = 215 ms.
1ère tentative
C'était plus lent que toutes les requêtes d'Erik que j'ai énumérées ici ... au moins en termes de temps écoulé.
SELECT SUM(p.Rows) -
(
SELECT COUNT(*)
FROM dbo.Users AS u
WHERE u.Age >= 18
)
FROM sys.objects o
JOIN sys.partitions p
ON p.object_id = o.object_id
WHERE p.index_id < 2
AND o.name = 'Users'
AND SCHEMA_NAME(o.schema_id) = 'dbo'
GROUP BY o.schema_id, o.name
Temps d'exécution SQL Server: temps CPU = 0 ms, temps écoulé = 0 ms. (1 ligne (s) retournée)
Tableau "Table de travail". Nombre de balayages 0, lectures logiques 0, lectures physiques 0, lectures anticipées 0, lectures logiques 0, lob lectures physiques 0, lob lectures anticipées 0. Table 'sysrowsets'. Nombre de balayages 2, lectures logiques 10, lectures physiques 0, lectures anticipées 0, lectures logiques 0, lob physiques lectures 0, lob lectures anticipées 0. Table 'sysschobjs'. Nombre de balayages 1, lectures logiques 4, lectures physiques 0, lectures anticipées 0, lectures logiques 0, lob lectures physiques 0, lob lectures anticipées 0. Tableau "Utilisateurs". Nombre de balayages 1, lectures logiques 201567, lectures physiques 0, lectures anticipées 0, lectures logiques 0, lob lectures physiques 0, lob lectures anticipées 0.
Temps d'exécution SQL Server: temps CPU = 593 ms, temps écoulé = 598 ms.
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 scan 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é.
DECLARE @Total INT;
SELECT @Total = SUM(p.Rows)
FROM sys.objects o
JOIN sys.partitions p
ON p.object_id = o.object_id
WHERE p.index_id < 2
AND o.name = 'Users'
AND SCHEMA_NAME(o.schema_id) = 'dbo'
GROUP BY o.schema_id, o.name
SELECT @Total - COUNT(*)
FROM dbo.Users AS u
WHERE u.Age >= 18
Temps d'exécution SQL Server: temps CPU = 0 ms, temps écoulé = 0 ms. Tableau "Table de travail". Nombre de balayages 0, lectures logiques 0, lectures physiques 0, lectures anticipées 0, lectures logiques 0, lob lectures physiques 0, lob lectures anticipées 0. Table 'sysrowsets'. Nombre de balayages 2, lectures logiques 10, lectures physiques 0, lectures anticipées 0, lectures logiques 0, lob physiques lectures 0, lob lectures anticipées 0. Table 'sysschobjs'. Nombre de balayages 1, lectures logiques 4, lectures physiques 0, lectures anticipées 0, lectures logiques 0, lob lectures physiques 0, lob lectures anticipées 0.
Temps d'exécution SQL Server: temps CPU = 0 ms, temps écoulé = 1 ms. (1 ligne (s) retournée)
Tableau "Utilisateurs". Nombre de balayages 17, lectures logiques 201567, lectures physiques 0, lectures anticipées 0, lectures logiques 0, lob lectures physiques 0, lob lectures anticipées 0.
Temps d'exécution SQL Server: temps CPU = 1471 ms, temps écoulé = 98 ms.
Autres remarques: DBCC TRACEON n'est pas autorisé sur Stack Exchange Data Explorer, comme indiqué ci-dessous:
L'utilisateur "STACKEXCHANGE\svc_sede" n'est pas autorisé à exécuter DBCC TRACEON.
Utiliser des variables?
declare @int1 int = ( select count(*) from table_1 where bb <= 1 )
declare @int2 int = ( select count(*) from table_1 where bb is null )
select @int1 + @int2;
Par le commentaire peut sauter les variables
SELECT (select count(*) from table_1 where bb <= 1)
+ (select count(*) from table_1 where bb is null);
Une solution triviale consiste à calculer le nombre (*) - le nombre (âge> = 18):
SELECT
(SELECT COUNT(*) FROM Users) -
(SELECT COUNT(*) FROM Users WHERE Age >= 18);
Ou:
SELECT COUNT(*)
- COUNT(CASE WHEN Age >= 18)
FROM Users;
Bien utiliser SET ANSI_NULLS OFF;
SET ANSI_NULLS OFF;
SET STATISTICS TIME ON;
SET STATISTICS IO ON;
SELECT COUNT(*)
FROM dbo.Users AS u
WHERE age=NULL or age<18
Table 'Users'. Scan count 17, logical reads 201567
SQL Server Execution Times:
CPU time = 2344 ms, elapsed time = 166 ms.
C'est quelque chose qui vient de me venir à l'esprit.Juste exécuté cela dans https://data.stackexchange.com
Mais pas aussi efficace que @blitz_erik