Quelles sont les différentes manières de remplacer ISNULL () dans une clause WHERE qui utilise uniquement des valeurs littérales?

55

Ce dont il ne s'agit pas:

Ce n'est pas une question sur les requêtes fourre-tout qui acceptent la saisie de l'utilisateur ou utilisent des variables.

Cela concerne uniquement les requêtes ISNULL()utilisées dans la WHEREclause pour remplacer les NULLvaleurs par une valeur canary afin de les comparer à un prédicat, ainsi que différentes manières de réécrire ces requêtes pour qu'elles soient SARGable dans SQL Server.

Pourquoi ne pas vous asseoir 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 des utilisateurs dont l' NULLâge est égal ou inférieur à 18 ans.

SELECT COUNT(*)
FROM dbo.Users AS u
WHERE ISNULL(u.Age, 17) < 18;

Le plan de requête affiche l'analyse d'un index non clusterisé très réfléchi.

Des noisettes

L’opérateur d’analyse indique (grâce aux ajouts apportés au plan d’exécution réel dans les versions plus récentes de SQL Server) que nous lisons chaque ligne de stinkin '.

Des noisettes

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 à offrir d'autres suggestions. Je ne pense pas que ma réponse soit nécessairement la réponse, et il y a suffisamment de personnes intelligentes pour proposer des solutions de rechange qui pourraient être meilleures.

Si vous voulez jouer avec votre ordinateur, rendez-vous ici pour télécharger la base de données SO .

Merci!

Erik Darling
la source

Réponses:

57

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 ORnous 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 malarkeyau plan de requête.

Des noisettes

Notez également que Seek est exécuté deux fois ici, ce qui devrait être plus évident pour l'opérateur graphique:

Des noisettes

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

Des noisettes

Il effectue la même quantité de lectures (8233) que la ORrequê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 COUNTopé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);

Des noisettes

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.

Des noisettes

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.

Des noisettes

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.

Des noisettes

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.

Des noisettes

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

Des noisettes

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

Des noisettes

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 EXCEPTet INTERSECTplus souvent que moi.

Si vous avez vraiment besoin d'un chiffre que j'utilise COUNTdans 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 CASEexpression 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.

Des noisettes

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!

Erik Darling
la source
1
Les NOT EXISTS ( INTERSECT / EXCEPT )requêtes peuvent fonctionner sans les INTERSECT / EXCEPTparties: WHERE NOT EXISTS ( SELECT u.Age WHERE u.Age >= 18 );Une autre façon - qui utilise EXCEPT: 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).
Ypercubeᵀᴹ
Cela at-il été testé? 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!
Ypercubeᵀᴹ
@ ypercubeᵀᴹ voici le plan pour celui-là. C'est un peu différent, mais a des caractéristiques similaires aux UNION ALLplans (CPU 360ms, 11k lectures).
Erik Darling
Hey Erik, parcourait juste le monde de SQL et est venu dire "colonne calculée" juste pour vous ennuyer. <3
creuset
17

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:

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 après 10 essais (la requête était naturellement parallèle pour moi):

╔══════════╦════════════════════╦═══════════════╗
 cpu_time  total_elapsed_time  logical_reads 
╠══════════╬════════════════════╬═══════════════╣
     3532                 975          60830 
╚══════════╩════════════════════╩═══════════════╝

La requête qui a mieux fonctionné pour Erik a en réalité moins bien fonctionné 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 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 avec AGE IS NULLmais 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 NULLcar Agecar 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:

fil paresseux

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 NULLlignes du tableau en quatre compartiments. Une façon de faire est de créer un groupe 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 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:

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 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:

travail bien divisé

Sur une note finale, nous pouvons également appuyer sur le bouton easy et ajouter un CCI non clusterisé à la Agecolonne:

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;

Cela va être difficile à battre.

Joe Obbish
la source
7

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;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 (s))

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.

QUERY 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 (s))

Tableau 'Utilisateurs'. Nombre de balayages 17, lectures logiques 201567, lectures physiques 0, lectures à l'avance lues 0, lectures logiques lob 0, lectures physiques lob 0, lectures anticipées lob 0.

Temps d'exécution SQL Server: temps CPU = 2500 ms, temps écoulé = 147 ms.

QUERY 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 (s))

Tableau 'Utilisateurs'. Nombre de balayages 34, lectures logiques 403134, lectures physiques 0, lectures anticipées 0, lectures logiques lobées 0, lectures physiques lob 0, lectures anticipées lobé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 ... du 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 (s))

Table 'Table de travail'. Nombre de balayages 0, lectures logiques 0, lectures physiques 0, lectures à l'avance lues 0, lectures logiques lob 0, lectures lob physiques 0, lectures lob à l'avance 0. Table 'sysrowsets'. Nombre de balayages 2, lectures logiques 10, lectures physiques 0, lectures à l'avance 0, lectures lob logiques 0, lectures lob physiques 0, lectures lob à l'avance 0. Table 'sysschobjs'. Nombre de balayages 1, lectures logiques 4, lectures physiques 0, lectures anticipées 0, lectures logiques lob 0, lectures physiques lob 0, lectures lob ultérieures 0. Tableau 'Utilisateurs'. Nombre de balayages 1, lectures logiques 201567, lectures physiques 0, lectures à l'avance lues 0, lectures logiques lob 0, lectures physiques lob 0, lectures anticipées lob 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 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é.

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. Table 'Table de travail'. Nombre de balayages 0, lectures logiques 0, lectures physiques 0, lectures à l'avance lues 0, lectures logiques lob 0, lectures lob physiques 0, lectures lob à l'avance 0. Table 'sysrowsets'. Nombre de balayages 2, lectures logiques 10, lectures physiques 0, lectures à l'avance 0, lectures lob logiques 0, lectures lob physiques 0, lectures lob à l'avance 0. Table 'sysschobjs'. Nombre de balayages 1, lectures logiques 4, lectures physiques 0, lectures anticipées 0, lectures logiques lobées 0, lectures physiques lob 0, lectures anticipées lobées 0.

Temps d'exécution SQL Server: temps CPU = 0 ms, temps écoulé = 1 ms. (1 ligne (s) retournée (s))

Tableau 'Utilisateurs'. Nombre de balayages 17, lectures logiques 201567, lectures physiques 0, lectures à l'avance lues 0, lectures logiques lob 0, lectures physiques lob 0, lectures anticipées lob 0.

Temps d'exécution SQL Server: temps CPU = 1471 ms, temps écoulé = 98 ms.

Autres notes: DBCC TRACEON n'est pas autorisé sur Stack Exchange Data Explorer, comme indiqué ci-dessous:

L'utilisateur 'STACKEXCHANGE \ svc_sede' n'a pas l'autorisation d'exécuter DBCC TRACEON.

Dave Mason
la source
1
Ils n'ont probablement pas les mêmes index que moi, d'où les différences. Et qui sait? Peut-être que mon serveur domestique est sur un meilleur matériel;) Bonne réponse cependant!
Erik Darling
vous auriez dû utiliser la requête suivante pour votre premier essai (ce sera beaucoup plus rapide, car il élimine une grande partie du sys.objects-overhead): 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')
Thomas Franz
PS: sachez que les index en mémoire (NONCLUSTERED HASH) n’ont pas d’index id = 0/1, contrairement à un index de segment de mémoire / cluster commun)
Thomas Franz Le
1

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 ignorer les variables

SELECT (select count(*) from table_1 where bb <= 1) 
     + (select count(*) from table_1 where bb is null);
paparazzo
la source
3
Aussi:SELECT (select count(*) from table_1 where bb <= 1) + (select count(*) from table_1 where bb is null);
ypercubeᵀᴹ
3
Peut-être voudriez-vous essayer cela en vérifiant le processeur et les entrées / sorties. Indice: c'est la même chose qu'une des réponses d'Erik.
Brent Ozar
0

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 m’arrive à l’esprit. Je viens de l’exécuter dans https://data.stackexchange.com.

Mais pas aussi efficace que @blitz_erik bien

Biju jose
la source
0

Une solution triviale consiste à calculer le nombre (*) - 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;

Résultats ici

Salman A
la source