Gegeben das folgende Schema
CREATE TABLE categories
(
id UNIQUEIDENTIFIER PRIMARY KEY,
name NVARCHAR(50)
);
CREATE TABLE [group]
(
id UNIQUEIDENTIFIER PRIMARY KEY
);
CREATE TABLE logger
(
id UNIQUEIDENTIFIER PRIMARY KEY,
group_id UNIQUEIDENTIFIER,
uuid CHAR(17)
);
CREATE TABLE data
(
id UNIQUEIDENTIFIER PRIMARY KEY,
logger_uuid CHAR(17),
category_name NVARCHAR(50),
recorded_on DATETIME
);
Und die folgenden Regeln
- Jeder
data
Datensatz verweist auf alogger
und acategory
- Jeder
logger
wird immer eine habengroup
- Jeder
group
kann mehrerelogger
s haben - Ich möchte nur die zuletzt aufgezeichneten Daten zählen
category_name
ist nicht eindeutig pro Zeile, es ist nur eine Möglichkeit, einen bestimmten Datensatz einer Kategorie zuzuordnen, es id
ist wirklich nur ein Ersatzschlüssel.
Was wäre der optimale Weg, um eine Ergebnismenge wie zu erreichen
category_id | logger_group_count
--------------------------------
12345 4
67890 2
..... ...
dh zähle die Nr. von Gruppen für jede Kategorie, in der ein Logger Daten aufgezeichnet hat?
Als ersten Stich kam ich auf:
SELECT g.id, COUNT(DISTINCT(a.id)) AS logger_group_count
FROM categories g
LEFT OUTER JOIN data d ON d.category_name = g.name
INNER JOIN logger s ON s.uuid = d.logger_uuid
INNER JOIN group a ON a.id = s.group_id
GROUP BY g.id
Ist aber extrem langsam (~ 45s), data
hat 400k + Datensätze - hier ist der Abfrageplan und hier ist eine Geige zum Spielen.
Ich möchte sicherstellen, dass ich das Beste aus der Abfrage heraushole, bevor ich mich mit anderen Dingen wie der Hardwareauslastung usw. befasse. Die Azure SQL-Kosten können erheblich steigen (obwohl Sie möglicherweise nur ein wenig mehr Saft von Ihrer aktuellen Stufe benötigen). .
Antworten:
Sie verwenden eine neuere Version von SQL Server, sodass der aktuelle Plan viele Informationen enthält. Siehe das Warnschild am
SELECT
Bediener? Dies bedeutet, dass SQL Server eine Warnung generiert hat, die die Abfrageleistung beeinträchtigen kann. Sie sollten sich immer diese ansehen:Es gibt zwei Datentypkonvertierungen, die durch Ihr Schema verursacht werden. Aufgrund der Warnungen vermute ich, dass der Name tatsächlich ein
NVARCHAR(100)
und einlogger_uuid
istNCHAR(17)
. Das in der Frage angegebene Tabellenschema ist möglicherweise nicht korrekt. Sie sollten die Hauptursache für diese Konvertierungen verstehen und beheben. Einige Arten von Datentypkonvertierungen verhindern Indexsuchen, führen zu Problemen bei der Kardinalitätsschätzung und verursachen andere Probleme.Eine weitere wichtige Sache, die überprüft werden muss, sind Wartestatistiken. Sie können diese auch in den Details des
SELECT
Bedieners sehen. Hier ist das XML für Ihre Wartestatistiken und die von der Abfrage aufgewendete Zeit:Ich bin kein Cloud-Typ, aber es sieht so aus, als ob Ihre Abfrage eine CPU nicht vollständig aktivieren kann . Dies hängt wahrscheinlich mit Ihrer aktuellen Azure-Ebene zusammen. Die Abfrage benötigte bei der Ausführung nur etwa 10 Sekunden CPU, dauerte jedoch 67 Sekunden. Ich glaube, dass 50 Sekunden dieser Zeit damit verbracht wurden, gedrosselt zu werden, und 7 Sekunden dieser Zeit wurden Ihnen gegeben, aber für andere Abfragen verwendet, die gleichzeitig ausgeführt wurden. Die schlechte Nachricht ist, dass die Abfrage langsamer ist, als es aufgrund Ihrer Stufe sein könnte. Die gute Nachricht ist, dass eine Reduzierung der CPU zu einer 5-fachen Reduzierung der Laufzeit führen kann. Mit anderen Worten, wenn Sie die Abfrage dazu bringen können, 1 Sekunde CPU zu verwenden, wird möglicherweise eine Laufzeit von etwa 5 Sekunden angezeigt.
Als Nächstes können Sie die Eigenschaft Aktuelle Zeitstatistik in Ihren Bedienerdetails überprüfen, um festzustellen, wo die CPU-Zeit verbracht wurde. Ihr Plan verwendet den Zeilenmodus, sodass die CPU-Zeit für einen Operator die Summe der Zeit ist, die dieser Operator sowie seine untergeordneten Elemente verbringen. Dies ist ein relativ einfacher Plan, sodass es nicht lange dauert, festzustellen, dass der Clustered-Index-Scan
logger_data
6527 ms CPU-Zeit benötigt. Der Loop-Join, der ihn aufruft, benötigt 10006 ms CPU-Zeit, sodass die gesamte CPU Ihrer Abfrage in diesem Schritt verbraucht wird. Ein weiterer Hinweis darauf, dass bei diesem Schritt etwas schief geht, finden Sie in der Dicke der relativen Pfeile:Von diesem Operator werden viele Zeilen zurückgegeben, daher lohnt es sich, sich die Details anzusehen. Wenn Sie sich die tatsächliche Anzahl der Zeilen für den Clustered-Index-Scan ansehen, sehen Sie, dass 14088885 Zeilen zurückgegeben und 14100798 Zeilen gelesen wurden. Die Kardinalität der Tabelle beträgt jedoch nur 484803 Zeilen. Intuitiv scheint das ziemlich ineffizient zu sein, oder? Der Clustered-Index-Scan gibt weit mehr als die Anzahl der Zeilen in der Tabelle zurück. Ein anderer Plan mit einem anderen Join-Typ oder einer anderen Zugriffsmethode in der Tabelle ist wahrscheinlich effizienter.
Warum hat SQL Server so viele Zeilen gelesen und zurückgegeben? Der Clustered-Index befindet sich auf der Innenseite einer verschachtelten Schleife. Es gibt 38 Zeilen, die von der Außenseite der Schleife zurückgegeben werden (der Scan in der
logger
Tabelle), sodass der Scan beilogger_data
38 Mal ausgeführt wird. 484803 * 38 = 18422514, was ziemlich nahe an der Anzahl der gelesenen Zeilen liegt. Warum hat SQL Server einen solchen Plan gewählt, der sich so ineffizient anfühlt? Es wird sogar geschätzt, dass 57 Scans der Tabelle durchgeführt werden. Der Plan, den Sie erhalten haben, war also wahrscheinlich effizienter als vermutet.Sie haben sich vielleicht gefragt, warum
TOP
Ihr Plan einen Operator enthält. SQL Server eingeführt , um eine Reihe Ziel , wenn eine Abfrage - Plan für Ihre Abfrage zu erstellen. Dies ist möglicherweise detaillierter als gewünscht. In der Kurzversion muss SQL Server jedoch nicht immer alle Zeilen eines Clustered-Index-Scans zurückgeben. Manchmal kann es vorzeitig beendet werden, wenn nur eine feste Anzahl von Zeilen benötigt wird und diese Zeilen gefunden werden, bevor das Ende des Scans erreicht ist. Ein Scan ist nicht so teuer, wenn er vorzeitig beendet werden kann, sodass die Bedienerkosten durch eine Formel abgezinst werden, wenn ein Zeilenziel vorliegt. Mit anderen Worten, SQL Server erwartet, den Clustered-Index 57 Mal zu scannen, geht jedoch davon aus, dass die benötigte einzelne Zeile sehr schnell gefunden wird. Aufgrund des Vorhandenseins von benötigt es nur eine einzige Zeile von jedem ScanTOP
Operator.Sie können Ihre Abfrage beschleunigen, indem Sie das Abfrageoptimierungsprogramm dazu ermutigen, einen Plan auszuwählen, der die
logger_data
Tabelle nicht 38 Mal scannt . Dies kann so einfach sein wie das Eliminieren der Datentypkonvertierungen. Dadurch könnte SQL Server eine Indexsuche anstelle eines Scans durchführen. Wenn nicht, korrigieren Sie die Conversions und erstellen Sie einen Deckungsindex fürlogger_data
:Das Abfrageoptimierungsprogramm wählt einen Plan basierend auf den Kosten aus. Durch Hinzufügen dieses Index ist es unwahrscheinlich, dass der langsame Plan erstellt wird, der viele Scans für logger_data ausführt, da der Zugriff auf die Tabelle über eine Indexsuche anstelle eines Clustered-Index-Scans billiger ist.
Wenn Sie den Index nicht hinzufügen können, können Sie einen Abfragehinweis hinzufügen, um die Einführung von Zeilenzielen zu deaktivieren :
USE HINT('DISABLE_OPTIMIZER_ROWGOAL'))
. Sie sollten dies nur tun, wenn Sie sich mit dem Konzept der Reihenziele wohl fühlen und diese verstehen. Das Hinzufügen dieses Hinweises sollte zu einem anderen Plan führen, aber ich kann nicht sagen, wie effizient er sein wird.quelle
Stellen Sie zunächst sicher, dass in jeder Tabelle alle Kandidatenschlüssel deklariert und Fremdschlüssel erzwungen sind:
Ich habe auch einen nicht eindeutigen Clustered-Index zu
logger_data
on hinzugefügtlogger_uuid, recorded_on
.Beachten Sie dann, dass die größte Aufgabe in Ihrem Ausführungsplan das Scannen der 484.836 Zeilen in der Datentabelle ist. Da Sie nur an der neuesten Lesung für einen bestimmten Logger interessiert sind und derzeit nur 48 Logger vorhanden sind, ist es effizienter, diesen vollständigen Scan durch 48 Singleton-Suchvorgänge zu ersetzen:
Der Ausführungsplan lautet:
dbfiddle
Sie sollten Ihre Instanz auch von 2017 RTM auf das neueste kumulative Update patchen.
quelle
Warum brauchen Sie den Join on Group?
Warum ist Kategorien g?
Ich hoffe, dass Sie im wirklichen Leben die Fremdschlüssel deklarieren.
Sie sollten einen Index für jede dieser Verknüpfungsspalten haben.
quelle
Problembereiche sind:
Improper data type
: Wenn Datentyp istINT
weniger Datenseite das bedeutet und nichtindex fragmentation
, wenn es sich umNewSequentialID
das bedeutet ,more data page
undno index fragmentation
mitUNIQUEIDENTIFIER
Ihnen beide problem.So INT Daten erhalten Typ ist die ideale Wahl.Data type and length of both column should be same in relationship column
:a.category_name = g.NAME
Logger_data Clustered-Index-Scan im Plan schlägt beispielsweise vor, dass beide Spaltenlängen 50 oder 100 betragen sollten, damit Optimizer keine Zeit damit verbringen muss.Convert_Implicit
Noch besser ist, dass die Beziehung mit dem Datentyp int wie CategoryID int` definiert wird.Denormalization
, in Ihrem Beispiel kann ich nicht sagen, wie?Versuchen Sie unten Abfrage,
Ich mag
@Paparazzi
Idee, also habe ich sie aufgenommen.Ich denke, Plan ist besser als dein. Mit der obigen Korrektur und Indexabstimmung wird es noch besser abschneiden.
Sie müssen hier korrigieren,
order by s.group_id
Sollte es sein ,order by DateOrIDcolumn desc
die neuesten record.with Ihre Probe gibt ich nicht in der Lage bin , wie man aus aktueller Platte zu finden.Beachten Sie auch, dass
partition by d.category_name
dies hätte sein sollenpartition by d.CatgoryID
.quelle
Dank einer großartigen Antwort von @JoeObbish konnte ich den Abfrageplan besser verstehen und herausfinden, wo es Probleme gab und welche Indizes ich verwenden konnte, um ihn zu verbessern. Dazwischen haben sich die Torpfosten ein wenig geändert, da ich vergessen habe zu erwähnen, dass dies nur für die neuesten Messwerte von jedem Logger gelten muss, z. B. wenn
logger_a
Daten unter aufgezeichnet wurdencategory_x @ 11:50
undcategory_y @ 11:51
ich dies nur als zählen möchtecategory_y
.Hier ist das resultierende SQL
Dies ist jedoch immer noch eine teure Abfrage, da die folgenden Indizes angewendet werden
Geht von ~ 25s bis ~ 3s und für eine Tabelle mit ~ 500k Zeilen. Ich bin ziemlich zufrieden damit und denke, dass es wahrscheinlich mehr Raum für Verbesserungen gibt, aber so wie es aussieht, ist dies gut genug.
Hier ist der endgültige Plan , weitere Vorschläge / Verbesserungen sind willkommen.
quelle