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 OR
die 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 .
Beachten Sie auch, dass der Suchlauf hier zweimal ausgeführt wird, was für den grafischen Operator eigentlich klarer sein sollte:
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.
Es führt die gleiche Anzahl von Lesevorgängen aus (8233) wie die OR
Abfrage, 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 COUNT
Vorgä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);
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.
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.
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 APPLY
wir mit der Magie von nichts doofes tun müssen, um einen größtenteils vollständig parallelen Plan zu haben.
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 COUNT
Zeug 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.
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 );
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);
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 EXCEPT
und INTERSECT
öfter als ich.
Wenn Sie wirklich nur eine Zählung benötigen, die
ich COUNT
in 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 CASE
Ausdruck 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.
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!
NOT EXISTS ( INTERSECT / EXCEPT )
Abfragen können auch ohne die folgendenINTERSECT / EXCEPT
Teile ausgeführt werden:WHERE NOT EXISTS ( SELECT u.Age WHERE u.Age >= 18 );
Eine andere Methode, die Folgendes verwendetEXCEPT
: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).