Database
 sql >> Datenbank >  >> RDS >> Database

Zwei Partitionierungseigenheiten

Die Tabellenpartitionierung in SQL Server ist im Wesentlichen eine Möglichkeit, mehrere physische Tabellen (Rowsets) wie eine einzelne Tabelle aussehen zu lassen. Diese Abstraktion wird vollständig vom Abfrageprozessor durchgeführt, ein Design, das die Dinge für Benutzer einfacher macht, aber komplexe Anforderungen an den Abfrageoptimierer stellt. Dieser Beitrag befasst sich mit zwei Beispielen, die die Fähigkeiten des Optimierers in SQL Server 2008 und höher übersteigen.

Kolumnenreihenfolgeangelegenheiten beitreten

Dieses erste Beispiel zeigt die textuelle Reihenfolge von ON -Klauselbedingungen können sich auf den beim Join partitionierter Tabellen erstellten Abfrageplan auswirken. Zunächst benötigen wir ein Partitionierungsschema, eine Partitionierungsfunktion und zwei Tabellen:

CREATE PARTITION FUNCTION PF (integer) 
AS RANGE RIGHT
FOR VALUES 
	(
	10000, 20000, 30000, 40000, 50000,
	60000, 70000, 80000, 90000, 100000,
	110000, 120000, 130000, 140000, 150000
	);
 
CREATE PARTITION SCHEME PS 
AS PARTITION PF 
ALL TO ([PRIMARY]);
GO
CREATE TABLE dbo.T1
(
    c1 integer NOT NULL,
    c2 integer NOT NULL,
    c3 integer NOT NULL,
 
    CONSTRAINT PK_T1
    PRIMARY KEY CLUSTERED (c1, c2, c3)
    ON PS (c1)
);
 
CREATE TABLE dbo.T2
(
    c1 integer NOT NULL,
    c2 integer NOT NULL,
    c3 integer NOT NULL,
 
    CONSTRAINT PK_T2
    PRIMARY KEY CLUSTERED (c1, c2, c3)
    ON PS (c1)
);

Als nächstes laden wir beide Tabellen mit 150.000 Zeilen. Die Daten spielen keine große Rolle; In diesem Beispiel wird eine standardmäßige Numbers-Tabelle mit allen ganzzahligen Werten von 1 bis 150.000 als Datenquelle verwendet. Beide Tabellen werden mit denselben Daten geladen.

INSERT dbo.T1 WITH (TABLOCKX)
    (c1, c2, c3)
SELECT
    N.n * 1,
    N.n * 2,
    N.n * 3
FROM dbo.Numbers AS N
WHERE
    N.n BETWEEN 1 AND 150000;
 
INSERT dbo.T2 WITH (TABLOCKX)
    (c1, c2, c3)
SELECT
    N.n * 1,
    N.n * 2,
    N.n * 3
FROM dbo.Numbers AS N
WHERE
    N.n BETWEEN 1 AND 150000;

Unsere Testabfrage führt eine einfache innere Verknüpfung dieser beiden Tabellen durch. Auch hier ist die Abfrage nicht wichtig oder soll besonders realistisch sein, sie wird verwendet, um einen seltsamen Effekt beim Verbinden von partitionierten Tabellen zu demonstrieren. Die erste Form der Abfrage verwendet ein ON Klausel in der Spaltenreihenfolge c3, c2, c1 geschrieben:

SELECT *
FROM dbo.T1 AS T1
JOIN dbo.T2 AS T2
    ON t1.c3 = t2.c3 
    AND t1.c2 = t2.c2 
    AND t1.c1 = t2.c1;

Der für diese Abfrage erstellte Ausführungsplan (auf SQL Server 2008 und höher) bietet einen parallelen Hash-Join mit geschätzten Kosten von 2,6953 :

Das ist etwas unerwartet. Beide Tabellen haben einen gruppierten Index in der Reihenfolge (c1, c2, c3), partitioniert durch c1, also würden wir einen Merge-Join erwarten, der die Indexreihenfolge nutzt. Versuchen wir, den ON zu schreiben stattdessen in der Reihenfolge (c1, c2, c3):

SELECT *
FROM dbo.T1 AS T1
JOIN dbo.T2 AS T2
    ON t1.c1 = t2.c1
    AND t1.c2 = t2.c2
    AND t1.c3 = t2.c3;

Der Ausführungsplan verwendet jetzt den erwarteten Merge-Join mit geschätzten Kosten von 1,64119 (nach unten von 2,6953 ). Der Optimierer entscheidet auch, dass es sich nicht lohnt, die parallele Ausführung zu verwenden:

Da der Merge-Join-Plan eindeutig effizienter ist, können wir versuchen, einen Merge-Join für das ursprüngliche ON zu erzwingen Klauselreihenfolge mit einem Abfragehinweis:

SELECT *
FROM dbo.T1 AS T1
JOIN dbo.T2 AS T2
    ON t1.c3 = t2.c3 
    AND t1.c2 = t2.c2 
    AND t1.c1 = t2.c1
OPTION (MERGE JOIN);

Der resultierende Plan verwendet wie gewünscht einen Merge-Join, bietet aber auch Sortierungen für beide Eingaben und geht zurück auf die Verwendung von Parallelität. Die geschätzten Kosten für diesen Plan betragen satte 8,71063 :

Beide Sortieroperatoren haben dieselben Eigenschaften:

Der Optimierer denkt, dass der Merge-Join seine Eingaben in der strikten schriftlichen Reihenfolge des ON sortiert haben muss -Klausel, wodurch explizite Sortierungen eingeführt werden. Der Optimierer ist sich bewusst, dass ein Merge-Join erfordert, dass seine Eingaben auf die gleiche Weise sortiert werden, aber er weiß auch, dass die Spaltenreihenfolge keine Rolle spielt. Merge Join on (c1, c2, c3) ist mit Eingaben, die nach (c3, c2, c1) sortiert sind, genauso zufrieden wie mit Eingaben, die nach (c2, c1, c3) oder einer beliebigen anderen Kombination sortiert sind.

Leider wird diese Argumentation im Abfrageoptimierer gebrochen, wenn es um Partitionierung geht. Dies ist ein Optimiererfehler das wurde in SQL Server 2008 R2 und höher behoben, obwohl das Ablaufverfolgungsflag 4199 wird benötigt, um den Fix zu aktivieren:

SELECT *
FROM dbo.T1 AS T1
JOIN dbo.T2 AS T2
    ON t1.c3 = t2.c3 
    AND t1.c2 = t2.c2 
    AND t1.c1 = t2.c1
OPTION (QUERYTRACEON 4199);

Normalerweise würden Sie dieses Ablaufverfolgungsflag mit DBCC TRACEON aktivieren oder als Startoption, weil der QUERYTRACEON Hinweis ist für die Verwendung mit 4199 nicht dokumentiert. Das Ablaufverfolgungsflag ist in SQL Server 2008 R2, SQL Server 2012 und SQL Server 2014 CTP1 erforderlich.

Unabhängig davon, wie das Flag aktiviert ist, erzeugt die Abfrage jetzt den optimalen Merge-Join, unabhängig von ON Klauselreihenfolge:

Es gibt keine Lösung für SQL Server 2008 , besteht die Problemumgehung darin, ON zu schreiben Satz in der „richtigen“ Reihenfolge! Wenn Sie auf SQL Server 2008 auf eine solche Abfrage stoßen, versuchen Sie, einen Merge-Join zu erzwingen, und sehen Sie sich die Sortierungen an, um die „richtige“ Schreibweise für ON Ihrer Abfrage zu bestimmen Klausel.

Dieses Problem tritt in SQL Server 2005 nicht auf, da diese Version partitionierte Abfragen mit APPLY implementiert hat Modell:

Der SQL Server 2005-Abfrageplan fügt jeweils eine Partition aus jeder Tabelle hinzu, wobei eine In-Memory-Tabelle (der konstante Scan) verwendet wird, die zu verarbeitende Partitionsnummern enthält. Jede Partition wird separat auf der inneren Seite des Joins zusammengeführt, und der Optimierer von 2005 ist intelligent genug, um zu erkennen, dass ON Die Spaltenreihenfolge der Klausel spielt keine Rolle.

Dieser neueste Plan ist ein Beispiel für einen verbundenen Merge-Join , eine Funktion, die beim Wechsel von SQL Server 2005 zur neuen Partitionierungsimplementierung in SQL Server 2008 verloren ging. Ein Vorschlag zu Verbinden, um verbundene Zusammenführungsverknüpfungen wiederherzustellen, wurde als Won’t Fix geschlossen.

Gruppieren nach Reihenfolge ist wichtig

Die zweite Besonderheit, die ich betrachten möchte, folgt einem ähnlichen Thema, bezieht sich aber auf die Reihenfolge der Spalten in einem GROUP BY -Klausel anstelle von ON Klausel eines inneren Joins. Wir brauchen eine neue Tabelle, um Folgendes zu demonstrieren:

CREATE TABLE dbo.T3
(
    RowID       integer IDENTITY NOT NULL,
    UserID      integer NOT NULL,
    SessionID   integer NOT NULL,
    LocationID  integer NOT NULL,
 
    CONSTRAINT PK_T3
    PRIMARY KEY CLUSTERED (RowID)
    ON PS (RowID)
);
 
INSERT dbo.T3 WITH (TABLOCKX)
    (UserID, SessionID, LocationID)
SELECT
    ABS(CHECKSUM(NEWID())) % 50,
    ABS(CHECKSUM(NEWID())) % 30,
    ABS(CHECKSUM(NEWID())) % 10
FROM dbo.Numbers AS N
WHERE
    N.n BETWEEN 1 AND 150000;

Die Tabelle hat einen ausgerichteten Nonclustered-Index, wobei "ausgerichtet" einfach bedeutet, dass sie auf die gleiche Weise partitioniert ist wie der Clustered-Index (oder Heap):

CREATE NONCLUSTERED INDEX nc1
ON dbo.T3 (UserID, SessionID, LocationID)
ON PS (RowID);

Unsere Testabfrage gruppiert Daten über die drei Nonclustered-Indexspalten und gibt eine Anzahl für jede Gruppe zurück:

SELECT LocationID, UserID, SessionID, COUNT_BIG(*)
FROM dbo.T3
GROUP BY LocationID, UserID, SessionID;

Der Abfrageplan scannt den Nonclustered-Index und verwendet ein Hash-Match-Aggregat, um Zeilen in jeder Gruppe zu zählen:

Es gibt zwei Probleme mit Hash Aggregate:

  1. Es ist ein blockierender Operator. Es werden keine Zeilen an den Client zurückgegeben, bis alle Zeilen aggregiert wurden.
  2. Es erfordert eine Speicherzuteilung, um die Hash-Tabelle zu halten.

In vielen realen Szenarien würden wir hier ein Stream Aggregate bevorzugen, da dieser Operator nur pro Gruppe blockiert und keine Speicherzuweisung erfordert. Mit dieser Option würde die Client-Anwendung früher anfangen, Daten zu empfangen, müsste nicht warten, bis Speicher gewährt wird, und der SQL-Server kann den Speicher für andere Zwecke verwenden.

Wir können verlangen, dass der Abfrageoptimierer ein Stream-Aggregat für diese Abfrage verwendet, indem wir eine OPTION (ORDER GROUP) hinzufügen Abfragehinweis. Daraus ergibt sich folgender Ausführungsplan:

Der Sort-Operator blockiert vollständig und erfordert auch eine Speicherzuweisung, sodass dieser Plan schlechter zu sein scheint als die einfache Verwendung eines Hash-Aggregats. Aber warum wird die Sorte benötigt? Die Eigenschaften zeigen, dass die Zeilen in der von unserem GROUP BY angegebenen Reihenfolge sortiert werden Klausel:

Diese Sortierung wird erwartet weil die Partitionsausrichtung des Index (ab SQL Server 2008) bedeutet, dass die Partitionsnummer als führende Spalte des Index hinzugefügt wird. Tatsächlich sind die nicht gruppierten Indexschlüssel (Partition, Benutzer, Sitzung, Speicherort) aufgrund der Partitionierung. Zeilen im Index werden weiterhin nach Benutzer, Sitzung und Ort sortiert, aber nur innerhalb jeder Partition.

Wenn wir die Abfrage auf eine einzelne Partition beschränken, sollte der Optimierer in der Lage sein, den Index zu verwenden, um ein Stream-Aggregat ohne Sortierung zu füttern. Falls dies eine Erklärung erfordert, bedeutet die Angabe einer einzelnen Partition, dass der Abfrageplan alle anderen Partitionen aus dem Nonclustered-Index-Scan eliminieren kann, was zu einem Strom von Zeilen führt, die nach (Benutzer, Sitzung, Speicherort) geordnet sind.

Wir können diese Partitionsentfernung explizit mit $PARTITION erreichen Funktion:

SELECT LocationID, UserID, SessionID, COUNT_BIG(*)
FROM dbo.T3
WHERE $PARTITION.PF(RowID) = 1
GROUP BY LocationID, UserID, SessionID;

Leider verwendet diese Abfrage immer noch ein Hash-Aggregat mit geschätzten Plankosten von 0,287878 :

Der Scan erstreckt sich jetzt nur über eine Partition, aber die Reihenfolge (Benutzer, Sitzung, Standort) hat dem Optimierer nicht geholfen, ein Stream-Aggregat zu verwenden. Sie könnten einwenden, dass die Sortierung (Benutzer, Sitzung, Ort) nicht hilfreich ist, weil GROUP BY -Klausel ist (location, user, session), aber die Schlüsselreihenfolge spielt für eine Gruppierungsoperation keine Rolle.

Lassen Sie uns einen ORDER BY hinzufügen Klausel in der Reihenfolge der Indexschlüssel, um den Punkt zu beweisen:

SELECT LocationID, UserID, SessionID, COUNT_BIG(*)
FROM dbo.T3
WHERE $PARTITION.PF(RowID) = 1
GROUP BY LocationID, UserID, SessionID
ORDER BY UserID, SessionID, LocationID;

Beachten Sie, dass ORDER BY -Klausel stimmt mit der Schlüsselreihenfolge des nicht gruppierten Index überein, obwohl die GROUP BY Klausel nicht. Der Ausführungsplan für diese Abfrage ist:

Jetzt haben wir das Stream-Aggregat, nach dem wir gesucht haben, mit geschätzten Plankosten von 0,0423925 (im Vergleich zu 0,287878 für den Hash Aggregate-Plan – fast 7-mal mehr).

Die andere Möglichkeit, hier ein Stream-Aggregat zu erreichen, besteht darin, GROUP BY neu zu ordnen Spalten, die mit den nicht gruppierten Indexschlüsseln übereinstimmen:

SELECT LocationID, UserID, SessionID, COUNT_BIG(*)
FROM dbo.T3 AS T1
WHERE $PARTITION.PF(RowID) = 1
GROUP BY UserID, SessionID, LocationID;

Diese Abfrage erzeugt denselben Stream Aggregate-Plan, der direkt oben gezeigt wird, mit genau denselben Kosten. Diese Empfindlichkeit gegenüber GROUP BY Die Spaltenreihenfolge ist spezifisch für partitionierte Tabellenabfragen in SQL Server 2008 und höher.

Sie können erkennen, dass die Hauptursache des Problems hier dem vorherigen Fall mit einem Merge Join ähnelt. Sowohl Merge Join als auch Stream Aggregate erfordern Eingaben, die nach den Join- oder Aggregationsschlüsseln sortiert sind, aber keiner kümmert sich um die Reihenfolge dieser Schlüssel. Ein Merge-Join auf (x, y, z) empfängt genauso gerne Zeilen, die nach (y, z, x) oder (z, y, x) geordnet sind, und das Gleiche gilt für Stream Aggregate.

Diese Einschränkung des Optimierers gilt auch für DISTINCT unter den gleichen Umständen. Die folgende Abfrage führt zu einem Hash Aggregate-Plan mit geschätzten Kosten von 0,286539 :

SELECT DISTINCT LocationID, UserID, SessionID
FROM dbo.T3 AS T1
WHERE $PARTITION.PF(RowID) = 1;

Wenn wir den DISTINCT schreiben Spalten in der Reihenfolge der Nonclustered-Indexschlüssel…

SELECT DISTINCT UserID, SessionID, LocationID
FROM dbo.T3 AS T1
WHERE $PARTITION.PF(RowID) = 1;

… werden wir mit einem Stream Aggregate-Plan mit Kosten von 0,041455 belohnt :

Zusammenfassend ist dies eine Einschränkung des Abfrageoptimierers in SQL Server 2008 und höher (einschließlich SQL Server 2014 CTP 1), die nicht durch die Verwendung des Ablaufverfolgungsflags 4199 behoben wird wie es beim Merge-Join-Beispiel der Fall war. Das Problem tritt nur bei partitionierten Tabellen mit einem GROUP BY auf oder DISTINCT über drei oder mehr Spalten mit einem ausgerichteten partitionierten Index, wobei eine einzelne Partition verarbeitet wird.

Wie beim Merge Join-Beispiel stellt dies einen Rückschritt gegenüber dem Verhalten von SQL Server 2005 dar. SQL Server 2005 fügte keinen impliziten führenden Schlüssel zu partitionierten Indizes hinzu, indem ein APPLY verwendet wurde Technik statt. In SQL Server 2005 verwenden alle hier dargestellten Abfragen $PARTITION um ein einzelnes Partitionsergebnis in Abfrageplänen anzugeben, die eine Partitionseliminierung durchführen, und Stream-Aggregate ohne Neuordnung des Abfragetexts zu verwenden.

Die Änderungen an der Verarbeitung von partitionierten Tabellen in SQL Server 2008 verbesserten die Leistung in mehreren wichtigen Bereichen, hauptsächlich im Zusammenhang mit der effizienten parallelen Verarbeitung von Partitionen. Leider hatten diese Änderungen Nebenwirkungen, die in späteren Versionen nicht alle behoben wurden.