Löschen Sie die Leistung für LOB-Daten in SQL Server

16

Diese Frage bezieht sich auf diesen Forenthread .

Ausführen von SQL Server 2008 Developer Edition auf meiner Workstation und einem Virtual Machine-Cluster mit zwei Knoten in Enterprise Edition, in dem ich auf "Alpha-Cluster" verweise.

Die Zeit, die zum Löschen von Zeilen mit einer varbinary (max) -Spalte benötigt wird, hängt direkt von der Länge der Daten in dieser Spalte ab. Das mag zunächst intuitiv klingen, aber nach einer Untersuchung widerspricht es meinem Verständnis, wie SQL Server Zeilen im Allgemeinen löscht und mit dieser Art von Daten umgeht.

Das Problem rührt von einem Lösch-Timeout-Problem (> 30 Sekunden) her, das wir in unserer .NET-Webanwendung sehen, aber ich habe es für diese Diskussion vereinfacht.

Wenn ein Datensatz gelöscht wird, markiert SQL Server ihn als einen Geist, der zu einem späteren Zeitpunkt nach dem Festschreiben der Transaktion von einer Geisterbereinigungsaufgabe bereinigt werden soll (siehe Paul Randals Blog ). Bei einem Test, bei dem drei Zeilen mit 16 KB, 4 MB und 50 MB Daten in einer varbinären (maximalen) Spalte gelöscht werden, wird dies sowohl auf der Seite mit dem zeileninternen Teil der Daten als auch in der Transaktion angezeigt Log.

Was mir seltsam vorkommt, ist, dass beim Löschen auf allen LOB-Datenseiten X-Sperren gesetzt werden und die Zuordnung der Seiten im PFS aufgehoben wird. Ich sehe dies im Transaktionslog, sowie mit sp_lockund den Ergebnissen der dm_db_index_operational_statsDMV ( page_lock_count).

Dies führt zu einem E / A-Engpass auf meiner Workstation und unserem Alpha-Cluster, wenn sich diese Seiten nicht bereits im Puffercache befinden. In der Tat ist die page_io_latch_wait_in_msvon derselben DMV praktisch die gesamte Dauer des Löschvorgangs und die page_io_latch_wait_countentspricht der Anzahl der gesperrten Seiten. Für die 50-MB-Datei auf meiner Workstation bedeutet dies mehr als 3 Sekunden, wenn mit einem leeren Puffercache ( checkpoint/ dbcc dropcleanbuffers) begonnen wird, und ich habe keinen Zweifel, dass dies bei starker Fragmentierung und unter Last länger dauern würde.

Ich habe versucht sicherzustellen, dass es nicht nur Platz im Cache zuweist, der diese Zeit in Anspruch nimmt. Ich habe 2 GB Daten aus anderen Zeilen eingelesen, bevor ich den Löschvorgang anstelle der checkpointMethode ausgeführt habe, die mehr ist, als dem SQL Server-Prozess zugewiesen ist. Ich bin mir nicht sicher, ob das ein gültiger Test ist oder nicht, da ich nicht weiß, wie SQL Server die Daten umstellt. Ich nahm an, es würde immer das Alte zugunsten des Neuen verdrängen.

Außerdem werden die Seiten nicht verändert. Das kann ich mit ansehen dm_os_buffer_descriptors. Die Seiten sind nach dem Löschen sauber, während die Anzahl der geänderten Seiten für alle drei kleinen, mittleren und großen Löschvorgänge weniger als 20 beträgt. Ich habe auch die Ausgabe von DBCC PAGEfür eine Stichprobe der nachgeschlagenen Seiten verglichen , und es gab keine Änderungen (nur das ALLOCATEDBit wurde aus PFS entfernt). Sie werden nur freigegeben.

Um weiter zu beweisen, dass die Seitensuche / -freigaben das Problem verursachen, habe ich den gleichen Test mit einer Filestream-Spalte anstelle von Vanilla Varbinary (max) durchgeführt. Die Löschvorgänge waren unabhängig von der LOB-Größe zeitlich konstant.

Also zuerst meine akademischen Fragen:

  1. Warum muss SQL Server alle LOB-Datenseiten nachschlagen, um sie X zu sperren? Ist das nur ein Detail davon, wie Sperren im Speicher dargestellt werden (irgendwie mit der Seite gespeichert)? Dadurch hängt die Auswirkung der E / A stark von der Datengröße ab, wenn sie nicht vollständig zwischengespeichert wird.
  2. Warum sperrt das X überhaupt, nur um die Zuordnung aufzuheben? Reicht es nicht aus, nur das Indexblatt mit dem Zeilenabschnitt zu sperren, da die Freigabe die Seiten selbst nicht ändern muss? Gibt es eine andere Möglichkeit, an die LOB-Daten zu gelangen, vor denen die Sperre schützt?
  3. Warum sollten Sie die Seiten im Voraus freigeben, da für diese Art von Arbeit bereits eine Hintergrundaufgabe vorhanden ist?

Und vielleicht noch wichtiger, meine praktische Frage:

  • Gibt es eine Möglichkeit, Löschvorgänge anders durchzuführen? Mein Ziel ist es, unabhängig von der Größe konstante Zeitlöschvorgänge durchzuführen, ähnlich wie beim Filestream, bei dem die Bereinigung nachträglich im Hintergrund erfolgt. Ist es eine Konfigurationssache? Lagere ich seltsamerweise Dinge?

So reproduzieren Sie den beschriebenen Test (ausgeführt über das SSMS-Abfragefenster):

CREATE TABLE [T] (
    [ID] [uniqueidentifier] NOT NULL PRIMARY KEY,
    [Data] [varbinary](max) NULL
)

DECLARE @SmallID uniqueidentifier
DECLARE @MediumID uniqueidentifier
DECLARE @LargeID uniqueidentifier

SELECT @SmallID = NEWID(), @MediumID = NEWID(), @LargeID = NEWID()
-- May want to keep these IDs somewhere so you can use them in the deletes without var declaration

INSERT INTO [T] VALUES (@SmallID, CAST(REPLICATE(CAST('a' AS varchar(max)), 16 * 1024) AS varbinary(max)))
INSERT INTO [T] VALUES (@MediumID, CAST(REPLICATE(CAST('a' AS varchar(max)), 4 * 1024 * 1024) AS varbinary(max)))
INSERT INTO [T] VALUES (@LargeID, CAST(REPLICATE(CAST('a' AS varchar(max)), 50 * 1024 * 1024) AS varbinary(max)))

-- Do this before test
CHECKPOINT
DBCC DROPCLEANBUFFERS
BEGIN TRAN

-- Do one of these deletes to measure results or profile
DELETE FROM [T] WHERE ID = @SmallID
DELETE FROM [T] WHERE ID = @MediumID
DELETE FROM [T] WHERE ID = @LargeID

-- Do this after test
ROLLBACK

Hier sind einige Ergebnisse aus der Profilerstellung der Löschvorgänge auf meiner Workstation:

| Spaltentyp | Größe löschen | Dauer (ms) | Liest | Schreibt | CPU |
-------------------------------------------------- ------------------
| VarBinary | 16 KB | 40 | 13 | 2 | 0 |
| VarBinary | 4 MB | 952 | 2318 | 2 | 0 |
| VarBinary | 50 MB | 2976 | 28594 | 1 | 62 |
-------------------------------------------------- ------------------
| FileStream | 16 KB | 1 | 12 | 1 | 0 |
| FileStream | 4 MB | 0 | 9 | 0 | 0 |
| FileStream | 50 MB | 1 | 9 | 0 | 0 |

Wir können nicht unbedingt nur den Dateistream verwenden, weil:

  1. Unsere Datengrößenverteilung garantiert dies nicht.
  2. In der Praxis fügen wir Daten in vielen Teilen hinzu, und der Dateistream unterstützt keine teilweisen Aktualisierungen. Wir müssten darum herum entwerfen.

Update 1

Es wurde eine Theorie getestet, nach der die Daten als Teil des Löschvorgangs in das Transaktionsprotokoll geschrieben werden. Dies scheint jedoch nicht der Fall zu sein. Teste ich das falsch? Siehe unten.

SELECT MAX([Current LSN]) FROM fn_dblog(NULL, NULL)
--0000002f:000001d9:0001

BEGIN TRAN
DELETE FROM [T] WHERE ID = @ID

SELECT
    SUM(
        DATALENGTH([RowLog Contents 0]) +
        DATALENGTH([RowLog Contents 1]) +
        DATALENGTH([RowLog Contents 3]) +
        DATALENGTH([RowLog Contents 4])
    ) [RowLog Contents Total],
    SUM(
        DATALENGTH([Log Record])
    ) [Log Record Total]
FROM fn_dblog(NULL, NULL)
WHERE [Current LSN] > '0000002f:000001d9:0001'

Bei einer Datei mit einer Größe von mehr als 5 MB wurde dies zurückgegeben 1651 | 171860.

Außerdem würde ich erwarten, dass die Seiten selbst verschmutzt sind, wenn Daten in das Protokoll geschrieben werden. Es scheinen nur die Aufhebungen protokolliert zu werden, die mit den nach dem Löschen verschmutzten Objekten übereinstimmen.

Update 2

Ich habe eine Antwort von Paul Randal erhalten. Er bekräftigte, dass es alle Seiten lesen muss, um den Baum zu durchlaufen und herauszufinden, welche Seiten freigegeben werden sollen, und stellte fest, dass es keine andere Möglichkeit gibt, welche Seiten nachzuschlagen. Dies ist eine halbe Antwort auf 1 & 2 (obwohl dies nicht die Notwendigkeit von Sperren für Daten außerhalb der Reihe erklärt, aber das sind kleine Kartoffeln).

Frage 3 ist noch offen: Warum die Zuordnung der Seiten aufheben, wenn bereits eine Hintergrundaufgabe zum Bereinigen von Löschvorgängen vorhanden ist?

Und natürlich die alles entscheidende Frage: Gibt es eine Möglichkeit, dieses größenabhängige Löschverhalten direkt zu mildern (dh nicht zu umgehen)? Ich würde denken, dass dies ein häufigeres Problem ist, es sei denn, wir sind wirklich die einzigen, die 50-MB-Zeilen in SQL Server speichern und löschen. Arbeiten alle anderen da draußen mit einer Art Garbage Collection-Job daran herum?

Jeremy Rosenberg
quelle
Ich wünschte, es gäbe eine bessere Lösung, aber ich habe keine gefunden. Ich habe die Situation, große Volumes mit Zeilen unterschiedlicher Größe (bis zu 1 MB +) zu protokollieren, und ich habe einen "Lösch" -Prozess zum Löschen alter Datensätze. Da die Löschvorgänge so langsam waren, musste ich sie in zwei Schritte aufteilen - zuerst die Verweise zwischen Tabellen entfernen (was sehr schnell ist) und dann verwaiste Zeilen löschen. Der Löschvorgang dauerte durchschnittlich ca. 2,2 Sekunden / MB, um Daten zu löschen. Also musste ich natürlich die Konkurrenz reduzieren, also habe ich eine gespeicherte Prozedur mit "DELETE TOP (250)" in einer Schleife, bis keine Zeilen mehr gelöscht werden.
Abacus

Antworten:

5

Ich kann nicht genau sagen, warum das Löschen eines VARBINARY (MAX) so viel ineffizienter wäre als das Löschen eines Dateistreams, aber eine Idee, die Sie in Betracht ziehen könnten, wenn Sie nur versuchen, Zeitüberschreitungen in Ihrer Webanwendung beim Löschen dieser LOBS zu vermeiden. Sie können die VARBINARY (MAX) -Werte in einer separaten Tabelle speichern (nennen wir sie tblLOB), auf die in der Originaltabelle verwiesen wird (nennen wir diese tblParent).

Wenn Sie einen Datensatz löschen, können Sie ihn einfach aus dem übergeordneten Datensatz löschen und dann gelegentlich eine Speicherbereinigung durchführen, um die Datensätze in der LOB-Tabelle zu bereinigen. Während dieses Garbage Collection-Vorgangs kann es zu zusätzlicher Festplattenaktivität kommen, die jedoch zumindest vom Front-End-Web getrennt ist und auch außerhalb der Stoßzeiten ausgeführt werden kann.

Ian Chamberland
quelle
Vielen Dank. Das ist genau eine unserer Optionen. Die Tabelle ist ein Dateisystem, und wir sind gerade dabei, die Binärdaten in eine vollständig separate Datenbank aus dem Hierarchie-Meta zu trennen. Wir könnten entweder tun, was Sie gesagt haben, und die Hierarchiezeile löschen und einen GC-Prozess veranlassen, verwaiste LOB-Zeilen zu bereinigen. Oder Sie haben einen Löschzeitstempel mit den Daten, um dasselbe Ziel zu erreichen. Dies ist der Weg, den wir einschlagen können, wenn keine zufriedenstellende Antwort auf das Problem vorliegt.
Jeremy Rosenberg
1
Ich wäre vorsichtig, wenn ich nur einen Zeitstempel hätte, der darauf hinweist, dass er gelöscht ist. Das wird funktionieren, aber irgendwann wird in aktiven Zeilen viel Platz belegt sein. Abhängig davon, wie viel gelöscht wird, müssen Sie irgendwann eine Art von gc-Prozess ausführen, und es hat weniger Auswirkungen darauf, regelmäßig weniger zu löschen als gelegentlich viele.
Ian Chamberland