it-swarm.com.de

Der effizienteste Weg, um eine nach Top-Tabelle gruppierte Unterabfrage COUNT abzurufen?

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

  1. Jeder data Datensatz verweist auf ein logger und ein category
  2. Jedes logger hat immer ein group
  3. Jedes group kann mehrere loggers haben
  4. 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. 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
.....          ...

z. 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

Aber ist extrem langsam (~ 45s), data hat 400k + Datensätze - hier ist der Abfrageplan und hier ist a Geige zum Spielen.

Ich möchte sicherstellen, dass ich das Beste aus der Abfrage heraushole, bevor ich mich mit anderen Dingen wie Hardware-Auslastung usw. befasse. Die Azure SQL-Kosten können erheblich steigen (auch wenn Sie möglicherweise nur etwas mehr Saft von Ihrer aktuellen Stufe benötigen). .

7
James

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 den letzten Messwert von jedem Logger, z. wenn logger_a aufgezeichnete Daten unter category_x @ 11:50 und category_y @ 11:51 Ich möchte dies nur als category_y.

Hier ist das resultierende SQL

;WITH logger_data AS (
  SELECT 
    category_name,
    logger_uuid,
    recorded_on,
    RN = ROW_NUMBER() OVER (PARTITION BY logger_uuid ORDER BY recorded_on DESC)
  FROM data
)
SELECT c.id, count(DISTINCT l.group_id) FROM categories c
INNER JOIN logger_data d on d.category_name = c.name
INNER JOIN logger l ON l.uuid = d.logger_uuid
WHERE RN = 1
GROUP BY c.id

Dies ist jedoch immer noch eine teure Abfrage, da die folgenden Indizes angewendet werden

CREATE CLUSTERED INDEX ix_latest ON "dbo"."data"
(
    logger_uuid,
    recorded_on DESC
)
GO
CREATE CLUSTERED INDEX ix_groups ON "dbo"."logger"
(
    group_id
)

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 das endgültige Plan , alle anderen Vorschläge/Verbesserungen sind willkommen.

0
James

Sie verwenden eine neuere Version von SQL Server, sodass der aktuelle Plan viele Informationen enthält. Siehe das Warnzeichen auf dem Operator SELECT? Dies bedeutet, dass SQL Server eine Warnung generiert hat, die die Abfrageleistung beeinträchtigen kann. Sie sollten sich immer diese ansehen:

<Warnings>
<PlanAffectingConvert ConvertIssue="Seek Plan" Expression="[s].[logger_uuid]=CONVERT_IMPLICIT(nchar(17),[d].[uuid],0)" />
<PlanAffectingConvert ConvertIssue="Seek Plan" Expression="CONVERT_IMPLICIT(nvarchar(100),[d].[name],0)=[g].[name]" />
</Warnings>

Es gibt zwei Datentypkonvertierungen, die durch Ihr Schema verursacht werden. Aufgrund der Warnungen vermute ich, dass der Name tatsächlich eine NVARCHAR(100) und logger_uuid Eine NCHAR(17) ist. 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 Operators SELECT sehen. Hier ist das XML für Ihre Wartestatistiken und die von der Abfrage aufgewendete Zeit:

<WaitStats>
<Wait WaitType="RESOURCE_GOVERNOR_IDLE" WaitTimeMs="49515" WaitCount="3773" />
<Wait WaitType="SOS_SCHEDULER_YIELD" WaitTimeMs="57164" WaitCount="2466" />
</WaitStats>
<QueryTimeStats ElapsedTime="67135" CpuTime="10007" />

Ich bin kein Cloud-Typ, aber es sieht so aus, als ob Ihre Abfrage nicht in der Lage ist, eine CPU vollständig einzuschalten . 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 auf 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:

(thick arrows

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. Von der Außenseite der Schleife werden 38 Zeilen zurückgegeben (der Scan in der Tabelle logger), sodass der Scan in logger_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 Ihr Plan einen Operator TOP enthält. SQL Server hat beim Erstellen eines Abfrageplans für Ihre Abfrage ein Zeilenziel eingeführt. 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 sind, wenn ein Zeilenziel vorhanden ist. 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 Operators TOP wird für jeden Scan nur eine einzige Zeile benötigt.

Sie können Ihre Abfrage beschleunigen, indem Sie den Abfrageoptimierer dazu ermutigen, einen Plan auszuwählen, der die Tabelle logger_data 38 Mal nicht durchsucht. 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 Konvertierungen und erstellen Sie einen Deckungsindex für logger_data:

CREATE INDEX IX ON logger_data (category_name, logger_uuid);

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 billiger ist als über einen Clustered-Index-Scan.

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.

8
Joe Obbish

Stellen Sie zunächst sicher, dass in jeder Tabelle alle Kandidatenschlüssel deklariert und Fremdschlüssel erzwungen sind:

CREATE TABLE dbo.categories
(
    id uniqueidentifier NOT NULL
        CONSTRAINT [UQ dbo.categories id]
        UNIQUE NONCLUSTERED,
    [name] nvarchar(50) NOT NULL 
        CONSTRAINT [PK dbo.categories name]
        PRIMARY KEY CLUSTERED
);

-- Choose a better name for this table
CREATE TABLE dbo.[group]
(
    id uniqueidentifier NOT NULL
        CONSTRAINT [PK dbo.group id]
        PRIMARY KEY CLUSTERED
);

CREATE TABLE dbo.logger
(
    id uniqueidentifier 
        CONSTRAINT [UQ dbo.logger id]
        UNIQUE NONCLUSTERED,
    group_id uniqueidentifier NOT NULL
        CONSTRAINT [FK dbo.group id]
        FOREIGN KEY (group_id)
        REFERENCES [dbo].[group] (id),
    uuid char(17) NOT NULL
        CONSTRAINT [PK dbo.logger uuid]
        PRIMARY KEY CLUSTERED
);

CREATE TABLE dbo.logger_data
(
    id uniqueidentifier 
        CONSTRAINT [PK dbo.logger_data id]
        PRIMARY KEY NONCLUSTERED,
    logger_uuid char(17) NOT NULL
        CONSTRAINT [FK dbo.logger_data uuid]
        FOREIGN KEY (logger_uuid)
        REFERENCES dbo.logger (uuid),
    category_name nvarchar(50) NOT NULL
        CONSTRAINT [dbo.logger_data name]
        FOREIGN KEY (category_name)
        REFERENCES dbo.categories ([name]),
    recorded_on datetime NOT NULL,

    INDEX [dbo.logger_data logger_uuid recorded_on] 
        CLUSTERED (logger_uuid, recorded_on)
);

Ich habe logger_data Auf logger_uuid, recorded_on Außerdem einen nicht eindeutigen Clustered-Index hinzugefügt.

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:

SELECT 
    category_id = C.id, 
    logger_group_count = COUNT_BIG(DISTINCT L.group_id)
FROM dbo.logger AS L
CROSS APPLY 
(
    -- Latest reading per logger
    SELECT TOP (1) 
        LD.recorded_on,
        LD.category_name
    FROM  dbo.logger_data AS LD
    WHERE LD.logger_uuid = L.uuid
    ORDER BY 
        LD.recorded_on DESC
) AS LDT1
JOIN dbo.categories AS C
    ON C.[name] = LDT1.category_name
GROUP BY
    C.id
ORDER BY
    C.id;

Der Ausführungsplan lautet:

(Estimated plan

dbfiddle

Sie sollten Ihre Instanz auch von 2017 RTM auf das neueste kumulative Update) patchen.

4
Paul White 9

Warum brauchen Sie den Join on Group?

Warum ist Kategorien g?

SELECT c.id, COUNT(DISTINCT(s.group_id)) AS logger_group_count 
FROM categories c
JOIN data d 
  ON d.category_name = c.name
JOIN logger s 
  ON s.uuid = d.logger_uuid
GROUP BY c.id  

Ich hoffe, dass Sie im wirklichen Leben die Fremdschlüssel deklarieren.

Sie sollten einen Index für jede dieser Verknüpfungsspalten haben.

0
paparazzo

Problembereiche sind:

  1. Improper data type: Wenn der Datentyp INT ist, bedeutet dies weniger Datenseite und kein index fragmentation, Wenn es NewSequentialID ist, bedeutet dies more data page Und no index fragmentation, Mit UNIQUEIDENTIFIER erhalten Sie beide Probleme. Der INT-Datentyp ist also die ideale Wahl.
  2. Data type and length of both column should be same in relationship column: Beispiel: a.category_name = g.NAME Logger_data Clustered-Index-Scan im Plan schlägt vor, dass beide Spaltenlängen 50 oder 100 betragen sollten, damit Optimizer keine Zeit für Convert_Implicit Aufwenden muss. Noch besser Die Beziehung sollte mit dem Datentyp int wie CategoryID int` definiert werden.
  3. Wenn diese Abfrage sehr wichtig ist und häufig verwendet wird, können Sie an Denormalization denken. In Ihrem Beispiel kann ich nicht sagen, wie?

Versuchen Sie unten Abfrage,

    SELECT g.id
    ,sum(CASE 
            WHEN rn = 1
                THEN 1
            ELSE 0
            END)
FROM categories g
INNER JOIN (
    SELECT d.category_name
        ,ROW_NUMBER() OVER (
            PARTITION BY d.category_name
            ,s.group_id ORDER BY s.group_id
            ) rn
    FROM data d
    INNER JOIN logger s ON s.uuid = d.logger_uuid
        --INNER JOIN [group] a ON a.id = s.group_id
    ) a ON a.category_name = g.NAME
GROUP BY g.id

Ich mag die Idee @Paparazzi, 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,

ROW_NUMBER()over(partition by d.category_name,a.id order by s.group_id )rn 

order by s.group_id, Es sollte order by DateOrIDcolumn desc Sein, das den neuesten Datensatz liefert. Mit Ihrem Beispiel kann ich nicht erkennen, wie der neueste Datensatz gefunden wird.

Beachten Sie auch partition by d.category_name, Dies sollte partition by d.CatgoryID Sein.

0
KumarHarsh