Wie kann ich für SQL Server gleichzeitige parallele Tabellenaktualisierungen beheben?

7

Ich muss alle Datensätze (Guids hinzufügen) in zwei (indizierten) leeren Spalten mit 150 Tabellen aktualisieren, wobei jede Tabelle etwa 50.000 Datensätze enthält (mithilfe eines Skripts 40.000 Aktualisierungen gleichzeitig in c # erstellen und auf dem Server veröffentlichen) und genau vier vorhanden sind Säulen.

Auf meinem lokalen Computer (16 GB RAM, 500 GB Samsung 850, SQL Server 2014, Core i5) dauert es insgesamt 13 Minuten , wenn ich versuche, 10 Tabellen parallel auszuführen. Wenn ich 5 ausführe, ist der Vorgang in nur 1,7 Minuten abgeschlossen .

Ich verstehe, dass auf Festplattenebene etwas beschäftigt ist, aber ich brauche Hilfe bei der Quantifizierung dieses großen Zeitunterschieds.

Gibt es eine genaue SQL Server-DB-Ansicht, mit der ich diese Diskrepanz überprüfen kann? Gibt es eine genaue Möglichkeit, für eine bestimmte Hardware herauszufinden, wie viele Tabellenaktualisierungen ich parallel ausführen kann? (Der echte Testserver verfügt über mehr RAM und Festplatten mit 10.000 U / min).

Kann jemand auf etwas verweisen, das ich auf dem SQL Server verbessern kann, um die Zeitabläufe für die parallel laufenden 10 Tabellen zu verbessern?

Ich habe bereits versucht, die Größe des automatischen Wachstums von 10 MB auf 100 MB zu erhöhen, wodurch sich die Länge der Festplattenwarteschlange verbessert (von etwa 5 auf 0,1), aber die Gesamtzeit wird dadurch nicht so stark verringert.

Ich habe genau die gleiche Frage zum Stackoverflow gestellt, aber bisher keine hilfreichen Antworten erhalten, sodass einige oder jede Einsicht / Hilfe immens hilfreich wäre. :) :)

Deb
quelle
Deb, erfassen Sie weitere Statistiken in der Datenbank, während die Abfragen ausgeführt werden, um den Engpass zu lokalisieren. Ich sehe, Sie haben sich auf die Länge der Festplattenwarteschlange bezogen, aber das ist nur ein kleines Stück eines viel größeren Bildes. Ich würde empfehlen, die CPU-Auslastung, die Speichernutzung und auch andere Festplattenstatistiken zu verfolgen (Schreibvorgänge / Sek., Ms pro Schreibvorgang wären ein guter Ausgangspunkt).
Chris Bergin
Am anderen Ende befindet sich ein Schreibkopf. Parallel landet nur in einer Warteschlange. Ihre beste Wette ist es, einen Thread beschäftigt zu halten. Ich benutze eine BlockingCollection - erstelle die Befehle auf dem Produzenten.
Paparazzo

Antworten:

3

Angesichts des Codes in Ihrer Antwort würden Sie höchstwahrscheinlich die Leistung verbessern, indem Sie die folgenden zwei Änderungen vornehmen:

  • Starten Sie den Abfragestapel mit BEGIN TRANund beenden Sie den Stapel mit COMMIT TRAN:

  • Verringern Sie die Anzahl der Aktualisierungen pro Stapel auf weniger als 5000, um eine Eskalation der Sperren zu vermeiden (die im Allgemeinen bei 5000 Sperren auftritt). Versuchen Sie 4500.

Wenn Sie diese beiden Schritte ausführen, sollten Sie die enorme Anzahl von Trans-Log-Schreibvorgängen und Sperr- / Entsperrvorgängen verringern, die Sie derzeit durch Ausführen einzelner DML-Anweisungen generieren.

Beispiel:

conn.Open();
using (SqlCommand cmd = new SqlCommand(
      @"BEGIN TRAN;
        UPDATE [TestTable] SET Column5 = 'some unique value' WHERE ID = 1;
        UPDATE [TestTable] SET Column5 = 'some unique value' WHERE ID = 2;
        ...
        UPDATE [TestTable] SET Column5 = 'some unique value' WHERE ID = 4500;
        COMMIT TRAN;
        ", conn));

AKTUALISIEREN

Die Frage ist etwas spärlich in den Details. Der Beispielcode wird nur in einer Antwort angezeigt .

Ein Bereich der Verwirrung besteht darin, dass in der Beschreibung die Aktualisierung von zwei Spalten erwähnt wird, der Beispielcode jedoch nur eine einzelne Spalte zeigt, die aktualisiert wird. Meine Antwort oben basierte auf dem Code, daher wird nur eine einzige Spalte angezeigt. Wenn wirklich zwei Spalten aktualisiert werden müssen, sollten beide Spalten in derselben UPDATEAnweisung aktualisiert werden :

conn.Open();
using (SqlCommand cmd = new SqlCommand(
      @"BEGIN TRAN;
        UPDATE [TestTable]
        SET    Column5 = 'some unique value',
               ColumnN = 'some unique value'
        WHERE  ID = 1;
        UPDATE [TestTable]
               SET Column5 = 'some unique value',
               SET ColumnN = 'some unique value'
        WHERE  ID = 2;
        ...
        UPDATE [TestTable]
               SET Column5 = 'some unique value',
               SET ColumnN = 'some unique value'
        WHERE  ID = 4500;
        COMMIT TRAN;
        ", conn));

Ein weiteres unklares Problem ist, woher die "eindeutigen" Daten stammen. In der Frage wird erwähnt, dass die eindeutigen Werte GUIDs sind. Werden diese in der App-Ebene generiert? Kommen sie aus einer anderen Datenquelle, die die App-Schicht kennt und die Datenbank nicht? Dies ist wichtig, da es abhängig von den Antworten auf diese Fragen sinnvoll sein kann, folgende Fragen zu stellen:

  1. Können die GUIDs stattdessen in SQL Server generiert werden?
  2. Wenn ja zu # 1, gibt es dann einen Grund, diesen Code aus App-Code zu generieren, anstatt eine einfache Batch-Schleife in T-SQL durchzuführen?

Wenn "Ja" zu # 1, aber der Code, aus welchem ​​Grund auch immer, in .NET generiert werden muss, können Sie Anweisungen verwenden NEWID()und generieren UPDATE, die BEGIN TRANfür Zeilenbereiche funktionieren. In diesem Fall benötigen Sie das / 'COMMIT` seitdem nicht mehr Jede Anweisung kann alle 4500 Zeilen auf einmal verarbeiten:

conn.Open();
using (SqlCommand cmd = new SqlCommand(
      @"UPDATE [TestTable]
        SET    Column5 = NEWID(),
               ColumnN = NEWID()
        WHERE  ID BETWEEN 1 and 4500;
        ", conn));

Wenn "Ja" zu Nummer 1 steht und es keinen wirklichen Grund dafür gibt, dass diese UPDATEs in .NET generiert werden, können Sie einfach Folgendes tun:

DECLARE @BatchSize INT = 4500, -- this could be an input param for a stored procedure
        @RowsAffected INT = 1, -- needed to enter loop
        @StartingID INT = 1; -- initial value

WHILE (@RowsAffected > 0)
BEGIN
  UPDATE TOP (@BatchSize) tbl
  SET    tbl.Column5 = NEWID(),
         tbl.ColumnN = NEWID()
  FROM   [TestTable] tbl
  WHERE  tbl.ID BETWEEN @StartingID AND (@StartingID + @BatchSize - 1);

  SET @RowsAffected = @@ROWCOUNT;
  SET @StartingID += @BatchSize;
END;

Der obige Code funktioniert nur, wenn die IDWerte nicht spärlich sind oder zumindest wenn die Werte keine größeren Lücken aufweisen als @BatchSize, sodass in jeder Iteration mindestens eine Zeile aktualisiert wird. Dieser Code setzt auch voraus, dass das IDFeld der Clustered Index ist. Diese Annahmen erscheinen angesichts des bereitgestellten Beispielcodes vernünftig.

Wenn die IDWerte jedoch große Lücken aufweisen oder wenn das IDFeld nicht der Clustered Index ist, können Sie nur nach Zeilen suchen, die noch keinen Wert haben:

DECLARE @BatchSize INT = 4500, -- this could be an input param for a stored procedure
        @RowsAffected INT = 1; -- needed to enter loop

WHILE (@RowsAffected > 0)
BEGIN
  UPDATE TOP (@BatchSize) tbl
  SET    tbl.Column5 = NEWID(),
         tbl.ColumnN = NEWID()
  FROM   [TestTable] tbl
  WHERE  tbl.Col1 IS NULL;

  SET @RowsAffected = @@ROWCOUNT;
END;

ABER , wenn "Nein" zu # 1 und die Werte aus einem guten Grund von .NET stammen, z. B. dass die eindeutigen Werte für jede IDbereits in einer anderen Quelle vorhanden sind, können Sie dies (über meinen ursprünglichen Vorschlag hinaus) noch beschleunigen, indem Sie angeben eine abgeleitete Tabelle:

conn.Open();
using (SqlCommand cmd = new SqlCommand(
      @"BEGIN TRAN;

        UPDATE tbl
        SET    tbl.Column5 = tmp.Col1,
               tbl.ColumnN = tmp.Col2
        FROM   [TestTable] tbl
        INNER JOIN (VALUES
          (1, 'some unique value A', 'some unique value B'),
          (2, 'some unique value C', 'some unique value D'),
          ...
          (1000, 'some unique value N1', 'some unique value N2')
                   ) tmp (ID, Col1, Col2)
                ON tmp.ID = tbl.ID;

        UPDATE tbl
        SET    tbl.Column5 = tmp.Col1,
               tbl.ColumnN = tmp.Col2
        FROM   [TestTable] tbl
        INNER JOIN (VALUES
          (1001, 'some unique value A2', 'some unique value B2'),
          (1002, 'some unique value C2', 'some unique value D2'),
          ...
          (2000, 'some unique value N3', 'some unique value N4')
                   ) tmp (ID, Col1, Col2)
                ON tmp.ID = tbl.ID;

        COMMIT TRAN;
        ", conn));

Ich glaube, dass die Anzahl der Zeilen, über die verbunden werden kann, auf VALUES1000 begrenzt ist. Deshalb habe ich zwei Sätze in einer expliziten Transaktion zusammengefasst. Sie können mit bis zu 4 Sätzen dieser UPDATEs testen , um 4000 pro Transaktion auszuführen und die Grenze von 5000 Sperren zu unterschreiten.

Solomon Rutzky
quelle
3

Basierend auf Ihrer eigenen Antwort sieht es so aus:

  1. Sie aktualisieren die erste und die zweite leere Spalte in separaten Aktualisierungsanweisungen

  2. Die leeren Spalten sind vom Datentyp varchar

Ich habe noch nicht genügend Repräsentanten auf DBA, um einen Kommentar abzugeben (ich habe ursprünglich die Version gesehen, die Sie auf Stack Overflow veröffentlicht haben), daher werde ich auf diese Annahme antworten.

In diesem Fall machen Sie möglicherweise einen häufigen Fehler bei Personen, die aus prozeduralen Sprachen zu SQL kommen: Sie müssen prozedural über SQL-Tabellen nachdenken und jede Zeile und Spalte einzeln aktualisieren.

SQL möchte , dass Sie satzbasierte Operationen ausführen , bei denen Sie SQL mitteilen, was Sie mit allen Zeilen in einer Abfrage / Anweisung tun möchten . Die SQL Server-Abfrage-Engine kann dann intern den besten Weg finden, um diese Änderung tatsächlich für alle Zeilen durchzuführen. Indem Sie Aktualisierungen zeilenweise durchführen, verhindern Sie, dass SQL Server das tut, was es am besten kann.

Es ist möglich, dass Sie sich dessen bewusst sind und die Art der Werte, die Sie aktualisieren müssen, macht zeilenweise Aktualisierungen unerlässlich, aber selbst dann, denke ich, könnten Sie beide Spalten für eine einzelne Tabellenzeile auf einmal aktualisieren und die halbieren Gesamtzahl der Aktualisierungen, die Sie durchführen müssen.

Wenn Sie ein gewisses Maß an Flexibilität hinsichtlich der eindeutigen Werte in Ihren Spalten haben, können Sie wahrscheinlich eine gesamte Tabelle mit einer einzigen SQL-Abfrage aktualisieren. Für echte GUID-Werte eine Abfrage wie folgt:

update TestTable
set    Column5 = NEWID()
       ,Column6 = NEWID()

gibt Ihnen eindeutige uniqueidentifierWerte in jeder Zelle. NEWID wird hier dokumentiert, wenn Sie es noch nicht gesehen haben. Sie müssten diese Abfrage dann nur noch 150 Mal für die einzelnen Tabellen wiederholen, was leicht parallelisiert werden könnte. Ich würde darauf wetten, dass es auch massiv schneller ist.

Wenn Sie etwas Zahlenbasiertes benötigen, können Sie eindeutige Zahlen wie die folgenden anwenden:

with cteNumbers as (
    select  Column5
            ,Column6
            ,Row_Number() over (order by id) as RowNo
    from TestTable
    )
update cteNumbers
set Column5=RowNo
    ,Column6=RowNo

Obwohl ich vermute, dass Sie das nicht versuchen; und in jedem Fall, wenn Ihre idSpalte eine automatische Inkrementierung ist int, können Sie dies einfach direkt verwenden, anstatt ein Row_Number()Over darüber zu generieren .

Wenn Sie etwas möchten, das auf inkrementierenden Zahlen basiert, aber nicht nur aus der Zahl besteht, können Sie einen Ausdruck um das herum erstellen, um das RowNozu erreichen, was Sie wollen.

Wo immer möglich, ist die Verwendung von satzbasierten Operationen eine absolute Notwendigkeit für eine effiziente SQL-Leistung.

Stuart J Cuthbertson
quelle
Vielen Dank für Ihre Antwort. Ich habe mich zwar mit dem SET-basierten Ansatz befasst, kann ihn jedoch nicht verwenden, da die Daten für diese Spalten von einem anderen Ort stammen. Ich aktualisiere nur im Grunde die neue GUID auf die ID des spezifischen Datensatzes.
Deb
2

Die Lösung gefunden. :) :)

Anstatt die 40K-Aktualisierungsabfragen gleichzeitig auszuführen (ich erstelle ein Aktualisierungsskript mit 40.000 Aktualisierungsanweisungen, wie im obigen Kommentar angegeben), wenn ich diese Anzahl auf die Hälfte reduziere - 20.000 Aktualisierungsabfragen auf einmal gibt es eine enorme Verbesserung - 10 Tabellen parallel dazu dauert es jetzt insgesamt 1,3 minuten - ich kann jetzt weitermachen.

Hier ist der Code, der das Update ausführt: Geben Sie hier die Bildbeschreibung ein

Jetzt wurde der Code geändert, um 20.000 gleichzeitig auszuführen.

Im Grunde wurden zuvor 10 (Threads) x 40.000 Aktualisierungsabfragen = 400.000 gleichzeitige Aktualisierungsabfragen beim ersten Ausführen und dann die restlichen 10 (Threads) x 10.000 Aktualisierungsabfragen ausgeführt, um alle 50.000 Datensätze in diesen 10 verschiedenen Typen zu aktualisieren.

Und jetzt tut es:

  1. 10 (Threads) X 20.000 Aktualisierungsabfragen = 200.000 gleichzeitige Aktualisierungsabfragen
  2. 10 (Threads) X 20.000 Aktualisierungsabfragen = 200.000 gleichzeitige Aktualisierungsabfragen
  3. 10 (Threads) X 10.000 Aktualisierungsabfragen = 100.000 Aktualisierungsabfragen

Ergebnis: Vorher: 13 Minuten , Nachher: ​​1,8 Minuten

Ich überprüfe jetzt, um die beste (schnellste!) Kombination herauszufinden, um diese 150 Tabellen mit mehreren Threads gleichzeitig zu aktualisieren. Wahrscheinlich kann ich eine höhere Anzahl von Tabellen parallel zu einer niedrigeren gleichzeitigen Aktualisierung wie 5k (von 20k) aktualisieren, aber ich werde jetzt damit beschäftigt sein, dies zu testen.

Deb
quelle
Sie sind sich nicht sicher, ob ich daraus eine vollwertige Antwort entwickeln kann, aber haben Sie nur als Idee in Betracht gezogen, dafür einen tabellenwertigen Parameter zu verwenden? Jeder Thread würde nur einen Teil der Daten in diesen Parameter laden und das Skript würde ungefähr so ​​aussehen : UPDATE t SET column5 = tvp.Value FROM [TestTable] AS t INNER JOIN @YourTableValuedParameter AS tvp ON t.id = tvp.id;. (Ich denke, @ srutzkys Vorschlag, die Anzahl der Updates pro Thread unter 5k zu halten, würde immer noch gelten.)
Andriy M
Bitte sagen Sie, dass Sie tatsächlich beide Spalten in einer Anweisung aktualisieren
Paparazzo
@Frisbee ja, beide Spalten gleichzeitig. Hier ist ein Beispiel in einem anderen Szenario, in dem ich mehr als zwei Spalten aktualisieren (tatsächlich von einer anderen Tabelle synchronisieren) musste. In der Tat gilt der Vorschlag von srutzky, die Anzahl der Aktualisierungen pro Thread unter 5.000 zu halten, immer noch, und ich habe seine Antwort jetzt als die beste Antwort für mein Szenario akzeptiert.
Deb
0

Es gibt keine magische Ansicht, die Ihnen sagt, wie viele Threads für die gegebene Hardware besser funktionieren, und wie bei allen guten Fragen lautet die Antwort "es kommt darauf an". Sie müssen berücksichtigen, dass zu diesem Zeitpunkt möglicherweise eine andere Last auftritt oder dass Ihre Abfrage in der CPU oder E / A stärker gewichtet ist. Aber was Sie tun können und wie es sich anhört, ist das Testen. Vielleicht möchten Sie auch eine andere Variable einfügen, MAXDOP.

Wenn möglich, lassen Sie in C # nur die Anzahl der Threads variabel sein (aus einer Datenbank oder einer Konfigurationsdatei lesen), dann können Sie Ihre Abfrage im laufenden Betrieb optimieren.

Obwohl es möglicherweise keine magische Ansicht gibt, können Sie wahrscheinlich die Wartezeit auf den Spids während jedes Laufs summieren, um zu sehen, wo die Wartezeiten und die Grenzen liegen.

Paulbarbin
quelle
MAXDOP scheint eine Option auf SQL Server-Ebene (höher) und keine Option auf Datenbankebene zu sein, und daher kann ich sie in der Produktionsdatenbank nicht ändern. Sind Sie absolut sicher, dass eine Erhöhung (jetzt verwende ich die Standardeinstellung) bei der Durchführung von Updates hilfreich ist? Vielleicht können Sie einige nützliche Links teilen, die Ihre Theorie vielleicht unterstützen .....
Deb
Ich habe jetzt mit MAXDOP = 4 (für die 4 physischen CPUs) getestet und es gibt keine Verbesserung. Zurücksetzen auf die Standardeinstellungen.
Deb
Ich habe nicht gesagt, dass es definitiv helfen würde, aber dass es nur eine weitere Variable ist, um eine bessere Konfiguration zu ermöglichen. Wenn Sie 4 Kerne haben und MAXDOP = 4 setzen, ist dies dasselbe, als wenn Sie es in Ruhe lassen. Lassen Sie uns das für eine Minute beiseite legen und uns auf den anderen Teil des Kommentars konzentrieren. Konfigurieren Sie die Anzahl der Threads und testen Sie sie für jede Umgebung. 4 Fäden sind möglicherweise die besten im Test und 20 sind möglicherweise die besten in der Produktion. Wir verwenden ein SSIS-Paket, das eine variable Eingabe ermöglicht, und wir können die Anzahl der Threads festlegen.
Paulbarbin
Dies ermöglicht uns zwei Dinge: die Fähigkeit, in verschiedenen Umgebungen zu optimieren, aber auch die Fähigkeit, die Produktion zu optimieren, wenn verschiedene Dinge vor sich gehen. Als wir anfingen, dauerte der ursprüngliche Prozess 24 Stunden. Wir haben es "parallelisiert" und den monatlichen Prozess auf 4 Stunden reduziert, ohne die gespeicherte Prozedur zu ändern. Später, als die Produktion mit anderen Prozessen beschäftigt war, mussten wir die Anzahl der Threads (und MAXDOP) reduzieren, damit andere Prozesse Zeit zum Abschluss haben.
Paulbarbin