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).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!UNION ALL
Pläne (360ms CPU, 11k Reads).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:
Ich habe die folgenden Ergebnisse aus sys.dm_exec_sessions über 10 Versuche erhalten (die Abfrage verlief für mich natürlich parallel):
Die Abfrage, die für Erik besser funktionierte, lief auf meinem Computer tatsächlich schlechter:
Ergebnisse aus 10 Studien:
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, dieAGE IS NULL
jedoch 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
NULL
für findet,Age
da 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:
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
NULL
Zeilen 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: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:
Die Ergebnisse aus zehn Studien:
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:
Abschließend können wir auch auf die einfache Schaltfläche klicken und der
Age
Spalte eine nicht gruppierte CCI hinzufügen :Die folgende Abfrage wird in 3 ms auf meinem Computer beendet:
Das wird schwer zu schlagen sein.
quelle
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;
undSET STATISTICS IO ON;
) verwendet, um die Abfragen zu testen. Als Referenz sind hier einige Abfragen und die CPU / IO-Statistiken:ABFRAGE 1
ABFRAGE 2
ABFRAGE 3
1. Versuch
Dies war langsamer als alle von Eriks Fragen, die ich hier aufgelistet habe ... zumindest in Bezug auf die verstrichene Zeit.
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.
Sonstige Hinweise: DBCC TRACEON ist im Stack Exchange Data Explorer nicht zulässig, wie unten angegeben:
quelle
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')
Variablen verwenden?
Per Kommentar können die Variablen übersprungen werden
quelle
SELECT (select count(*) from table_1 where bb <= 1) + (select count(*) from table_1 where bb is null);
Gut mit
SET ANSI_NULLS OFF;
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
quelle
Eine einfache Lösung besteht darin, count (*) - count (age> = 18) zu berechnen:
Oder:
Ergebnisse hier
quelle