Welche unterschiedlichen Möglichkeiten gibt es, ISNULL () in einer WHERE-Klausel zu ersetzen, die nur Literalwerte verwendet?


55

Worum es hier nicht geht:

Dies ist keine Frage zu Catch-All-Abfragen , die Benutzereingaben akzeptieren oder Variablen verwenden.

Hierbei handelt es sich ausschließlich um Abfragen, bei denen ISNULL()in der WHEREKlausel zum Ersetzen von NULLWerten durch einen kanarischen Wert zum Vergleich mit einem Prädikat verwendet wird, sowie um verschiedene Möglichkeiten, diese Abfragen so umzuschreiben, dass sie in SQL Server SARG-fähig sind .

Warum setzt du dich nicht dort hin?

Unsere Beispielabfrage bezieht sich auf eine lokale Kopie der Stack Overflow-Datenbank in SQL Server 2016 und sucht nach Benutzern mit einem NULLAlter oder einem Alter <18.

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

Der Abfrageplan zeigt einen Scan eines nachdenklichen Nonclustered-Index.

Nüsse

Der Scan-Operator zeigt (dank der Erweiterung des XML-Ausführungsplans in neueren Versionen von SQL Server), dass wir jede noch so unangenehme Zeile gelesen haben.

Nüsse

Insgesamt machen wir 9157 Lesevorgänge und verbrauchen ungefähr eine halbe Sekunde CPU-Zeit:

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.

Die Frage: Wie kann diese Abfrage umgeschrieben werden, um sie effizienter und möglicherweise sogar SARG-fähig zu machen?

Fühlen Sie sich frei, andere Vorschläge anzubieten. Ich glaube nicht, dass meine Antwort notwendigerweise die Antwort ist, und es gibt genug kluge Leute, um Alternativen zu finden, die besser sein könnten.

Wenn Sie auf Ihrem eigenen Computer mitspielen möchten, klicken Sie hier, um die SO-Datenbank herunterzuladen .

Vielen Dank!

Antworten:


57

Antwortbereich

Es gibt verschiedene Möglichkeiten, dies mit verschiedenen T-SQL-Konstrukten umzuschreiben. Wir werden uns die Vor- und Nachteile ansehen und unten einen allgemeinen Vergleich anstellen.

Erstens : MitOR

SELECT COUNT(*)
FROM dbo.Users AS u
WHERE u.Age < 18
OR u.Age IS NULL;

Durch ORdie Verwendung von wird ein effizienterer Suchplan erstellt, der die genaue Anzahl der von uns benötigten Zeilen liest a whole mess of malarkey, dem Abfrageplan jedoch das hinzufügt, was die technische Welt fordert .

Nüsse

Beachten Sie auch, dass der Suchlauf hier zweimal ausgeführt wird, was für den grafischen Operator eigentlich klarer sein sollte:

Nüsse

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.

Zweitens : Verwenden von abgeleiteten Tabellen mit UNION ALL Unsere Abfrage kann auch so umgeschrieben werden

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

Dies ergibt die gleiche Art von Plan, mit weitaus weniger Malarkey und einem offensichtlicheren Grad an Ehrlichkeit darüber, wie oft nach dem Index gesucht (gesucht?) Wurde.

Nüsse

Es führt die gleiche Anzahl von Lesevorgängen aus (8233) wie die ORAbfrage, spart jedoch etwa 100 ms CPU-Zeit.

CPU time = 313 ms,  elapsed time = 315 ms.

Sie müssen hier jedoch sehr vorsichtig sein, da bei einem Parallelversuch dieses Plans die beiden separaten COUNTVorgänge serialisiert werden, da sie jeweils als globales Skalaraggregat betrachtet werden. Wenn wir einen parallelen Plan mit Trace Flag 8649 erzwingen, wird das Problem offensichtlich.

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

Nüsse

Dies kann vermieden werden, indem Sie unsere Abfrage geringfügig ändern.

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

Jetzt sind beide Knoten, die einen Suchvorgang ausführen, vollständig parallelisiert, bis wir den Verkettungsoperator erreichen.

Nüsse

Für das, was es wert ist, hat die voll parallele Version einige gute Vorteile. Bei etwa 100 weiteren Lesevorgängen und etwa 90 ms zusätzlicher CPU-Zeit verkürzt sich die verstrichene Zeit auf 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.

Was ist mit CROSS APPLY? Keine Antwort ist vollständig ohne die Magie von CROSS APPLY!

Leider haben wir mehr Probleme mit 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);

Dieser Plan ist schrecklich. Dies ist die Art von Plan, mit der Sie enden, wenn Sie zuletzt zum St. Patrick's Day erscheinen. Obwohl sehr parallel, scannt es aus irgendeinem Grund den PK / CX. Ew. Der Plan kostet 2198 Query-Dollar.

Nüsse

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.

Das ist eine seltsame Wahl, denn wenn wir die Verwendung des nicht gruppierten Index erzwingen, sinken die Kosten erheblich auf 1798-Abfragewerte.

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, sucht! Schau dich da drüben an. Beachten Sie auch, dass CROSS APPLYwir mit der Magie von nichts doofes tun müssen, um einen größtenteils vollständig parallelen Plan zu haben.

Nüsse

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.

Cross Apply geht es am Ende besser, ohne dass das COUNTZeug drin ist .

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

Der Plan sieht gut aus, aber die Lesevorgänge und die CPU sind keine Verbesserung.

Nüsse

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.

Wenn Sie das Kreuz neu schreiben, um eine abgeleitete Verknüpfung zu erstellen, erhalten Sie genau dasselbe Ergebnis. Ich werde den Abfrageplan und die Statistikinformationen nicht erneut veröffentlichen - sie haben sich wirklich nicht geändert.

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;

Relationale Algebra : Um gründlich zu sein und Joe Celko davon abzuhalten, meine Träume zu verfolgen, müssen wir zumindest seltsame relationale Dinge ausprobieren. Hier geht nichts!

Ein Versuch mit 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 );

Nüsse

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.

Und hier ist ein Versuch mit 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);

Nüsse

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.

Es mag andere Möglichkeiten geben, diese zu schreiben, aber das überlasse ich den Leuten, die sie vielleicht benutzen EXCEPTund INTERSECTöfter als ich.

Wenn Sie wirklich nur eine Zählung benötigen, die ich COUNTin meinen Abfragen als Abkürzung verwende (lesen Sie: Ich bin zu faul, um mir manchmal komplexere Szenarien auszudenken). Wenn Sie nur eine Zählung benötigen, können Sie einen CASEAusdruck verwenden, um genau dasselbe zu tun.

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

Beide erhalten den gleichen Plan und haben die gleichen CPU- und Leseeigenschaften.

Nüsse

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.

Der Gewinner? In meinen Tests hat der erzwungene parallele Plan mit SUM über eine abgeleitete Tabelle die beste Leistung erbracht. Und ja, viele dieser Abfragen hätten durch Hinzufügen einiger gefilterter Indizes unterstützt werden können, um beide Prädikate zu berücksichtigen, aber ich wollte einige Experimente anderen überlassen.

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

Vielen Dank!


1
Die NOT EXISTS ( INTERSECT / EXCEPT )Abfragen können auch ohne die folgenden INTERSECT / EXCEPTTeile ausgeführt werden: WHERE NOT EXISTS ( SELECT u.Age WHERE u.Age >= 18 );Eine andere Methode, die Folgendes verwendet EXCEPT: SELECT COUNT(*) FROM (SELECT UserID FROM dbo.Users EXCEPT SELECT UserID FROM dbo.Users WHERE u.Age >= 18) AS u ;(wobei UserID die PK oder eine eindeutige Spalte (n) ist, die nicht null ist).
Ypercubeᵀᴹ

Wurde das getestet? 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) ;Tut mir leid, wenn ich in den von Ihnen getesteten Millionen Versionen etwas verpasst habe!
ypercubeᵀᴹ

@ ypercubeᵀᴹ hier ist der Plan für diesen. Es ist ein wenig anders, hat aber ähnliche Eigenschaften wie die UNION ALLPläne (360ms CPU, 11k Reads).
Erik Darling

Hey Erik, war gerade in der Welt von SQL unterwegs und kam herein, um "berechnete Spalte" zu sagen, nur um dich zu ärgern. <3
Tiegel

17

Ich war nicht imstande, eine 110-GB-Datenbank für nur eine Tabelle wiederherzustellen, also habe ich meine eigenen Daten erstellt . Die Altersverteilung sollte dem entsprechen, was sich auf dem Stapelüberlauf befindet, aber offensichtlich stimmt die Tabelle selbst nicht überein. Ich denke nicht, dass es ein zu großes Problem ist, da die Abfragen ohnehin die Indizes treffen werden. Ich teste auf einem 4-CPU-Computer mit SQL Server 2016 SP1. Beachten Sie, dass bei Abfragen, die so schnell beendet werden, der tatsächliche Ausführungsplan nicht berücksichtigt werden muss. Das kann die Sache ziemlich verlangsamen.

Ich begann damit, einige der Lösungen in Eriks exzellenter Antwort durchzugehen. Für dieses:

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

Ich habe die folgenden Ergebnisse aus sys.dm_exec_sessions über 10 Versuche erhalten (die Abfrage verlief für mich natürlich parallel):

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

Die Abfrage, die für Erik besser funktionierte, lief auf meinem Computer tatsächlich schlechter:

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

Ergebnisse aus 10 Studien:

╔══════════╦════════════════════╦═══════════════╗
 cpu_time  total_elapsed_time  logical_reads 
╠══════════╬════════════════════╬═══════════════╣
     5704                1636          60850 
╚══════════╩════════════════════╩═══════════════╝

Ich kann nicht sofort erklären, warum es so schlimm ist, aber es ist nicht klar, warum wir fast jeden Operator im Abfrageplan zwingen wollen, parallel zu verfahren. Im ursprünglichen Plan haben wir eine serielle Zone, die alle Zeilen mit findet AGE < 18. Es gibt nur ein paar tausend Zeilen. Auf meinem Computer erhalte ich 9 logische Lesevorgänge für diesen Teil der Abfrage sowie 9 ms der gemeldeten CPU-Zeit und der verstrichenen Zeit. Es gibt auch eine serielle Zone für das globale Aggregat für die Zeilen mit, die AGE IS NULLjedoch nur eine Zeile pro DOP verarbeitet. Auf meiner Maschine sind das nur vier Reihen.

Meiner Meinung nach ist es am wichtigsten, den Teil der Abfrage zu optimieren, der Zeilen mit einem NULLfür findet, Ageda Millionen dieser Zeilen vorhanden sind. Ich konnte keinen Index mit weniger Seiten erstellen, die die Daten abdeckten, als mit einem einfachen seitenkomprimierten Index für die Spalte. Ich gehe davon aus, dass es eine minimale Indexgröße pro Zeile gibt oder dass ein Großteil des Indexbereichs mit den von mir ausgeführten Tricks nicht vermieden werden kann. Wenn also ungefähr die gleiche Anzahl von logischen Lesevorgängen zum Abrufen der Daten erforderlich ist, besteht die einzige Möglichkeit, diese schneller zu machen, darin, die Abfrage paralleler zu gestalten. Dies muss jedoch anders erfolgen als bei Eriks Abfrage, die TF verwendet hat 8649. In der obigen Abfrage haben wir ein Verhältnis von 3,62 für die CPU-Zeit zur verstrichenen Zeit, was ziemlich gut ist. Ideal wäre ein Verhältnis von 4,0 auf meiner Maschine.

Ein möglicher Verbesserungsbereich besteht darin, die Arbeit gleichmäßiger auf die Fäden aufzuteilen. In der Abbildung unten sehen wir, dass eine meiner CPUs beschlossen hat, eine kleine Pause einzulegen:

Fauler Faden

Der Index-Scan ist einer der wenigen Operatoren, die parallel implementiert werden können, und wir können nichts dagegen tun, wie die Zeilen auf Threads verteilt werden. Es gibt auch einen Zufallsfaktor, aber ich habe durchgehend einen unterarbeiteten Faden gesehen. Eine Möglichkeit, dies zu umgehen, besteht darin, die Parallelität auf die harte Tour auszuführen: im inneren Teil eines Nested-Loop-Joins. Alles im Inneren einer verschachtelten Schleife wird seriell implementiert, aber viele serielle Threads können gleichzeitig ausgeführt werden. Solange wir eine günstige Parallelverteilungsmethode (wie Round Robin) erhalten, können wir genau steuern, wie viele Zeilen an jeden Thread gesendet werden.

Ich führe Abfragen mit DOP 4 aus, sodass ich die NULLZeilen in der Tabelle gleichmäßig in vier Buckets aufteilen muss . Eine Möglichkeit besteht darin, eine Reihe von Indizes für berechnete Spalten zu erstellen:

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

Ich bin nicht ganz sicher, warum vier separate Indizes etwas schneller sind als ein Index, aber genau das habe ich in meinen Tests festgestellt.

Um einen Plan für eine parallele verschachtelte Schleife zu erhalten, verwende ich das undokumentierte Ablaufverfolgungsflag 8649 . Ich werde den Code auch ein wenig seltsam schreiben, um das Optimierungsprogramm zu ermutigen, nicht mehr Zeilen als nötig zu verarbeiten. Nachfolgend finden Sie eine Implementierung, die anscheinend gut funktioniert:

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

Die Ergebnisse aus zehn Studien:

╔══════════╦════════════════════╦═══════════════╗
 cpu_time  total_elapsed_time  logical_reads 
╠══════════╬════════════════════╬═══════════════╣
     3093                 803          62008 
╚══════════╩════════════════════╩═══════════════╝

Mit dieser Abfrage haben wir ein Verhältnis von CPU zu abgelaufener Zeit von 3,85! Wir haben 17 ms von der Laufzeit entfernt und es hat nur 4 berechnete Spalten und Indizes gedauert, um dies zu tun! Jeder Thread verarbeitet insgesamt sehr nahe an der gleichen Anzahl von Zeilen, da jeder Index sehr nahe an der gleichen Anzahl von Zeilen liegt und jeder Thread nur einen Index durchsucht:

gut geteilte Arbeit

Abschließend können wir auch auf die einfache Schaltfläche klicken und der AgeSpalte eine nicht gruppierte CCI hinzufügen :

CREATE NONCLUSTERED COLUMNSTORE INDEX X_NCCI ON dbo.Users (Age);

Die folgende Abfrage wird in 3 ms auf meinem Computer beendet:

SELECT COUNT(*)
FROM dbo.Users AS u
WHERE u.Age < 18 OR u.Age IS NULL;

Das wird schwer zu schlagen sein.


7

Obwohl ich keine lokale Kopie der Stack Overflow-Datenbank habe, konnte ich einige Abfragen durchführen. Mein Gedanke war, die Anzahl der Benutzer aus einer Systemkatalogansicht zu ermitteln (im Gegensatz dazu, direkt die Anzahl der Zeilen aus der zugrunde liegenden Tabelle zu ermitteln). Ermitteln Sie dann die Anzahl der Zeilen, die den Kriterien von Erik entsprechen (oder auch nicht), und rechnen Sie einfach nach.

Ich habe den Stack Exchange Data Explorer (zusammen mit SET STATISTICS TIME ON;und SET STATISTICS IO ON;) verwendet, um die Abfragen zu testen. Als Referenz sind hier einige Abfragen und die CPU / IO-Statistiken:

ABFRAGE 1

--Erik's query From initial question.
SELECT COUNT(*)
FROM dbo.Users AS u
WHERE ISNULL(u.Age, 17) < 18;

SQL Server-Ausführungszeiten: CPU-Zeit = 0 ms, verstrichene Zeit = 0 ms. (1 Zeile (n) zurückgegeben)

Tabelle 'Benutzer'. Scananzahl 17, logische Lesevorgänge 201567, physische Lesevorgänge 0, Vorauslesevorgänge 2740, logische Lobs-Lesevorgänge 0, physikalische Lobs-Lesevorgänge 0, Lobs-Vorauslesevorgänge 0.

SQL Server-Ausführungszeiten: CPU-Zeit = 1829 ms, verstrichene Zeit = 296 ms.

ABFRAGE 2

--Erik's "OR" query.
SELECT COUNT(*)
FROM dbo.Users AS u
WHERE u.Age < 18
OR u.Age IS NULL;

SQL Server-Ausführungszeiten: CPU-Zeit = 0 ms, verstrichene Zeit = 0 ms. (1 Zeile (n) zurückgegeben)

Tabelle 'Benutzer'. Scan-Anzahl 17, logische Lesevorgänge 201567, physische Lesevorgänge 0, Vorlesevorgänge 0, logische Vorlesevorgänge 0, physische Vorlesevorgänge 0, Vorlesevorgänge 0.

SQL Server-Ausführungszeiten: CPU-Zeit = 2500 ms, verstrichene Zeit = 147 ms.

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

SQL Server-Ausführungszeiten: CPU-Zeit = 0 ms, verstrichene Zeit = 0 ms. (1 Zeile (n) zurückgegeben)

Tabelle 'Benutzer'. Scan-Anzahl 34, logische Lesevorgänge 403134, physikalische Lesevorgänge 0, Vorauslesevorgänge 0, logische Lobs-Lesevorgänge 0, physikalische Lobs-Vorauslesevorgänge 0.

SQL Server-Ausführungszeiten: CPU-Zeit = 3156 ms, verstrichene Zeit = 215 ms.

1. Versuch

Dies war langsamer als alle von Eriks Fragen, die ich hier aufgelistet habe ... zumindest in Bezug auf die verstrichene Zeit.

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

SQL Server-Ausführungszeiten: CPU-Zeit = 0 ms, verstrichene Zeit = 0 ms. (1 Zeile (n) zurückgegeben)

Tabelle 'Arbeitstisch'. Scananzahl 0, logische Lesevorgänge 0, physische Lesevorgänge 0, Vorauslesevorgänge 0, logische Lobs-Lesevorgänge 0, physikalische Lobs-Lesevorgänge 0, Lobs-Vorauslesevorgänge 0. Tabelle 'sysrowsets'. Scananzahl 2, logische Lesevorgänge 10, physische Lesevorgänge 0, Vorlesevorgänge 0, logische Vorlesevorgänge 0, physische Vorlesevorgänge 0, Vorlesevorgänge 0. Tabelle 'sysschobjs'. Scananzahl 1, logische Lesevorgänge 4, physische Lesevorgänge 0, Vorlesevorgänge 0, logische Vorlesevorgänge 0, physische Vorlesevorgänge 0, Vorlesevorgänge 0. Tabelle 'Benutzer'. Scan-Anzahl 1, logische Lesevorgänge 201567, physische Lesevorgänge 0, Vorlesevorgänge 0, logische Vorlesevorgänge 0, physische Vorlesevorgänge 0, Vorlesevorgänge 0.

SQL Server-Ausführungszeiten: CPU-Zeit = 593 ms, verstrichene Zeit = 598 ms.

2. Versuch

Hier habe ich mich für eine Variable entschieden, um die Gesamtzahl der Benutzer zu speichern (anstelle einer Unterabfrage). Die Anzahl der Scans stieg im Vergleich zum ersten Versuch von 1 auf 17. Die logischen Lesevorgänge blieben unverändert. Die verstrichene Zeit ist jedoch erheblich gesunken.

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

SQL Server-Ausführungszeiten: CPU-Zeit = 0 ms, verstrichene Zeit = 0 ms. Tabelle 'Arbeitstisch'. Scananzahl 0, logische Lesevorgänge 0, physische Lesevorgänge 0, Vorauslesevorgänge 0, logische Lobs-Lesevorgänge 0, physikalische Lobs-Lesevorgänge 0, Lobs-Vorauslesevorgänge 0. Tabelle 'sysrowsets'. Scananzahl 2, logische Lesevorgänge 10, physische Lesevorgänge 0, Vorlesevorgänge 0, logische Vorlesevorgänge 0, physische Vorlesevorgänge 0, Vorlesevorgänge 0. Tabelle 'sysschobjs'. Scananzahl 1, logische Lesevorgänge 4, physische Lesevorgänge 0, Vorauslesevorgänge 0, logische Lobs-Lesevorgänge 0, physikalische Lobs-Lesevorgänge 0, Lobs-Vorauslesevorgänge 0.

SQL Server-Ausführungszeiten: CPU-Zeit = 0 ms, verstrichene Zeit = 1 ms. (1 Zeile (n) zurückgegeben)

Tabelle 'Benutzer'. Scan-Anzahl 17, logische Lesevorgänge 201567, physische Lesevorgänge 0, Vorlesevorgänge 0, logische Vorlesevorgänge 0, physische Vorlesevorgänge 0, Vorlesevorgänge 0.

SQL Server-Ausführungszeiten: CPU-Zeit = 1471 ms, verstrichene Zeit = 98 ms.

Sonstige Hinweise: DBCC TRACEON ist im Stack Exchange Data Explorer nicht zulässig, wie unten angegeben:

Der Benutzer 'STACKEXCHANGE \ svc_sede' hat keine Berechtigung, DBCC TRACEON auszuführen.


1
Sie haben wahrscheinlich nicht die gleichen Indizes wie ich, daher die Unterschiede. Und wer weiß? Vielleicht ist mein Heimserver auf besserer Hardware;) Tolle Antwort!
Erik Darling

Sie sollten die folgende Abfrage für Ihren ersten Versuch verwendet haben (wird viel schneller sein, da sie einen Großteil des sys.objects-Overheads entfernt): 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: Beachten Sie, dass In-Memory-Indizes (NONCLUSTERED HASH) keine Index-ID = 0/1 haben, wie es ein gemeinsamer Heap / Clustered-Index haben würde)
Thomas Franz

1

Variablen verwenden?

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;

Per Kommentar können die Variablen übersprungen werden

SELECT (select count(*) from table_1 where bb <= 1) 
     + (select count(*) from table_1 where bb is null);

3
Außerdem:SELECT (select count(*) from table_1 where bb <= 1) + (select count(*) from table_1 where bb is null);
ypercubeᵀᴹ

3
Vielleicht möchten Sie das versuchen, während Sie CPU & IO überprüfen. Hinweis: Es ist dasselbe wie eine von Eriks Antworten.
Brent Ozar

0

Gut mit 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.

Dies ist etwas, das mir gerade in den Sinn gekommen ist. Ich habe es gerade in https://data.stackexchange.com ausgeführt

Aber nicht so effizient wie @blitz_erik


0

Eine einfache Lösung besteht darin, count (*) - count (age> = 18) zu berechnen:

SELECT
    (SELECT COUNT(*) FROM Users) -
    (SELECT COUNT(*) FROM Users WHERE Age >= 18);

Oder:

SELECT COUNT(*)
     - COUNT(CASE WHEN Age >= 18)
FROM Users;

Ergebnisse hier

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.