Effizientes Aktualisieren einer Tabelle mit JOIN

8

Ich habe eine Tabelle mit den Details der Haushalte und eine andere mit den Details aller mit den Haushalten verbundenen Personen. Für die Haushaltstabelle habe ich einen Primärschlüssel definiert, der aus zwei Spalten besteht - [tempId,n]. Für die Personentabelle habe ich einen Primärschlüssel definiert, der mit 3 seiner Spalten definiert ist[tempId,n,sporder]

Unter Verwendung der Sortierung, die durch die gruppierte Indizierung von Primärschlüsseln vorgegeben ist, habe ich für jeden Haushalt [HHID]und jeden Personendatensatz eine eindeutige ID generiert [PERID](das folgende Snippet dient zum Generieren von PERID):

 ALTER TABLE dbo.persons
 ADD PERID INT IDENTITY
 CONSTRAINT [UQ dbo.persons HHID] UNIQUE;

Jetzt ist mein nächster Schritt, jede Person mit den entsprechenden Haushalten zu verbinden, dh; Karte a [PERID]zu a [HHID]. Der Zebrastreifen zwischen den beiden Tabellen basiert auf den beiden Spalten [tempId,n]. Dafür habe ich die folgende innere Join-Anweisung.

UPDATE t1
  SET t1.HHID = t2.HHID
  FROM dbo.persons AS t1
  INNER JOIN dbo.households AS t2
  ON t1.tempId = t2.tempId AND t1.n = t2.n;

Ich habe insgesamt 1928783 Haushaltsaufzeichnungen und 5239842 Personenaufzeichnungen. Die Ausführungszeit ist derzeit sehr hoch.

Nun meine Fragen:

  1. Ist es möglich, diese Abfrage weiter zu optimieren? Was sind allgemein die Daumenregeln für die Optimierung einer Join-Abfrage?
  2. Gibt es ein anderes Abfragekonstrukt, das das gewünschte Ergebnis mit einer besseren Ausführungszeit erzielen kann?

Ich habe den von SQL Server 2008 generierten Ausführungsplan für das gesamte Skript auf SQLPerformance.com hochgeladen

Sriramn
quelle

Antworten:

19

Ich bin mir ziemlich sicher, dass die Tabellendefinitionen in der Nähe davon liegen:

CREATE TABLE dbo.households
(
    tempId  integer NOT NULL,
    n       integer NOT NULL,
    HHID    integer IDENTITY NOT NULL,

    CONSTRAINT [UQ dbo.households HHID] 
        UNIQUE NONCLUSTERED (HHID),

    CONSTRAINT [PK dbo.households tempId, n]
    PRIMARY KEY CLUSTERED (tempId, n)
);

CREATE TABLE dbo.persons
(
    tempId  integer NOT NULL,
    sporder integer NOT NULL,
    n       integer NOT NULL,
    PERID   integer IDENTITY NOT NULL,
    HHID    integer NOT NULL,

    CONSTRAINT [UQ dbo.persons HHID]
        UNIQUE NONCLUSTERED (PERID),

    CONSTRAINT [PK dbo.persons tempId, n, sporder]
        PRIMARY KEY CLUSTERED (tempId, n, sporder)
);

Ich habe keine Statistiken für diese Tabellen oder Ihre Daten, aber das Folgende wird zumindest die Kardinalität der Tabelle korrekt einstellen (die Seitenzahlen sind eine Vermutung):

UPDATE STATISTICS dbo.persons 
WITH 
    ROWCOUNT = 5239842, 
    PAGECOUNT = 100000;

UPDATE STATISTICS dbo.households 
WITH 
    ROWCOUNT = 1928783, 
    PAGECOUNT = 25000;

Abfrageplananalyse

Die Abfrage, die Sie jetzt haben, lautet:

UPDATE P
SET HHID = H.HHID
FROM dbo.households AS H
JOIN dbo.persons AS P
    ON P.tempId = H.tempId
    AND P.n = H.n;

Dies erzeugt den ziemlich ineffizienten Plan:

Standardplan

Die Hauptprobleme in diesem Plan sind das Hash-Join und Sortieren. Für beide ist eine Speicherzuweisung erforderlich (der Hash-Join muss eine Hash-Tabelle erstellen, und die Sortierung benötigt Platz zum Speichern der Zeilen, während die Sortierung fortschreitet). Der Plan-Explorer zeigt, dass dieser Abfrage 765 MB gewährt wurden:

Memory Grant

Dies ist ziemlich viel Serverspeicher für eine Abfrage! Genauer gesagt wird diese Speicherzuweisung vor Beginn der Ausführung basierend auf Zeilenanzahl und Größenschätzungen festgelegt.

Wenn sich herausstellt, dass der Speicher zur Ausführungszeit nicht ausreicht, werden zumindest einige Daten für den Hash und / oder die Sortierung auf die physische Tempdb- Festplatte geschrieben. Dies wird als "Verschütten" bezeichnet und kann ein sehr langsamer Vorgang sein. Sie können diese Verschüttungen (in SQL Server 2008) mithilfe der Hash-Warnungen und Sortierwarnungen der Profiler-Ereignisse verfolgen .

Die Schätzung für die Build-Eingabe der Hash-Tabelle ist sehr gut:

Hash-Eingabe

Die Schätzung für die Sortiereingabe ist weniger genau:

Eingabe sortieren

Sie müssten Profiler verwenden, um dies zu überprüfen, aber ich vermute, dass die Sortierung in diesem Fall auf Tempdb übertragen wird. Es ist auch möglich, dass die Hash-Tabelle ebenfalls verschüttet wird, aber das ist weniger eindeutig.

Beachten Sie, dass der für diese Abfrage reservierte Speicher zwischen der Hash-Tabelle und der Sortierung aufgeteilt wird, da sie gleichzeitig ausgeführt werden. Die Plan-Eigenschaft "Speicherfraktionen" zeigt den relativen Betrag der Speicherzuweisung an, die voraussichtlich von jeder Operation verwendet wird.

Warum Sortieren und Hash?

Die Sortierung wird vom Abfrageoptimierer eingeführt, um sicherzustellen, dass Zeilen in der Reihenfolge der Clusterschlüssel beim Operator "Clustered Index Update" ankommen. Dies fördert den sequentiellen Zugriff auf die Tabelle, was häufig viel effizienter ist als der Direktzugriff.

Der Hash-Join ist eine weniger offensichtliche Wahl, da seine Eingaben ähnliche Größen haben (jedenfalls in erster Näherung). Hash-Join ist am besten geeignet, wenn eine Eingabe (diejenige, die die Hash-Tabelle erstellt) relativ klein ist.

In diesem Fall bestimmt das Kalkulationsmodell des Optimierers, dass der Hash-Join die billigere der drei Optionen ist (Hash, Merge, verschachtelte Schleifen).

Verbessernde Leistung

Das Kostenmodell macht es nicht immer richtig. Die Kosten für die parallele Zusammenführung werden tendenziell überschätzt, insbesondere wenn die Anzahl der Threads zunimmt. Wir können einen Merge-Join mit einem Abfragehinweis erzwingen:

UPDATE P
SET HHID = H.HHID
FROM dbo.households AS H
JOIN dbo.persons AS P
    ON P.tempId = H.tempId
    AND P.n = H.n
OPTION (MERGE JOIN);

Dies erzeugt einen Plan, der nicht so viel Speicher benötigt (da für den Merge-Join keine Hash-Tabelle erforderlich ist):

Zusammenführungsplan

Die problematische Sortierung ist immer noch vorhanden, da beim Zusammenführen von Joins nur die Reihenfolge der Join-Schlüssel (tempId, n) beibehalten wird, die Cluster-Schlüssel jedoch (tempId, n, sporder). Möglicherweise ist der Merge-Join-Plan nicht besser als der Hash-Join-Plan.

Verschachtelte Schleifen verbinden

Wir können auch versuchen, eine verschachtelte Schleife zu verbinden:

UPDATE P
SET HHID = H.HHID
FROM dbo.households AS H
JOIN dbo.persons AS P
    ON P.tempId = H.tempId
    AND P.n = H.n
OPTION (LOOP JOIN);

Der Plan für diese Abfrage lautet:

Plan für serielle verschachtelte Schleifen

Dieser Abfrageplan wird vom Kalkulationsmodell des Optimierers als der schlechteste angesehen, weist jedoch einige sehr wünschenswerte Funktionen auf. Erstens erfordert der Join verschachtelter Schleifen keine Speicherzuweisung. Zweitens kann die Schlüsselreihenfolge aus der PersonsTabelle beibehalten werden, sodass keine explizite Sortierung erforderlich ist. Möglicherweise ist dieser Plan relativ gut, vielleicht sogar gut genug.

Parallele verschachtelte Schleifen

Der große Nachteil des Plans für verschachtelte Schleifen besteht darin, dass er auf einem einzelnen Thread ausgeführt wird. Es ist wahrscheinlich, dass diese Abfrage von Parallelität profitiert, aber der Optimierer entscheidet, dass dies hier keinen Vorteil hat. Dies ist auch nicht unbedingt richtig. Leider gibt es keinen integrierten Abfragehinweis, um einen parallelen Plan zu erhalten, aber es gibt einen undokumentierten Weg:

UPDATE t1
  SET t1.HHID = t2.HHID
  FROM dbo.persons AS t1
  INNER JOIN dbo.households AS t2
  ON t1.tempId = t2.tempId AND t1.n = t2.n
OPTION (LOOP JOIN, QUERYTRACEON 8649);

Das Aktivieren des Ablaufverfolgungsflags 8649 mit dem QUERYTRACEONHinweis erzeugt diesen Plan:

Plan für parallele verschachtelte Schleifen

Jetzt haben wir einen Plan, der die Sortierung vermeidet, keinen zusätzlichen Speicher für den Join benötigt und Parallelität effektiv nutzt. Sie sollten feststellen, dass diese Abfrage viel besser funktioniert als die Alternativen.

Weitere Informationen zur Parallelität finden Sie in meinem Artikel Erzwingen eines Ausführungsplans für parallele Abfragen :

Paul White 9
quelle
1

Wenn Sie sich Ihren Abfrageplan ansehen, ist es möglich, dass Ihr eigentliches Problem nicht der Join an sich ist, sondern der eigentliche Aktualisierungsprozess.

Soweit ich sehen kann, aktualisieren Sie wahrscheinlich alle Personendatensätze in Ihrer Datenbank und aktualisieren die Indizes (ich kann nicht sehen, welche anderen Indizes dies hat, daher weiß ich nicht, ob dies ein Faktor sein könnte).

Wenn dies eine einmalige Aufgabe ist, können Sie die Indizes deaktivieren, das Update ausführen und die Indizes neu erstellen?

Sobald Sie die Daten ausgefüllt haben, können Sie Ihrer Abfrage eine where-Klausel hinzufügen, um nur die Datensätze zu aktualisieren, die aktualisiert werden müssen

TomGough
quelle