Ich habe ein Problem mit einer großen Anzahl von INSERTs, die meine SELECT-Operationen blockieren.
Schema
Ich habe einen Tisch wie diesen:
CREATE TABLE [InverterData](
[InverterID] [bigint] NOT NULL,
[TimeStamp] [datetime] NOT NULL,
[ValueA] [decimal](18, 2) NULL,
[ValueB] [decimal](18, 2) NULL
CONSTRAINT [PrimaryKey_e149e28f-5754-4229-be01-65fafeebce16] PRIMARY KEY CLUSTERED
(
[TimeStamp] DESC,
[InverterID] ASC
) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF
, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON
, ALLOW_PAGE_LOCKS = ON)
)
Ich habe auch diese kleine Hilfsprozedur, die es mir ermöglicht, mit dem Befehl MERGE einzufügen oder zu aktualisieren (bei Konflikt zu aktualisieren):
CREATE PROCEDURE [InsertOrUpdateInverterData]
@InverterID bigint, @TimeStamp datetime
, @ValueA decimal(18,2), @ValueB decimal(18,2)
AS
BEGIN
MERGE [InverterData] AS TARGET
USING (VALUES (@InverterID, @TimeStamp, @ValueA, @ValueB))
AS SOURCE ([InverterID], [TimeStamp], [ValueA], [ValueB])
ON TARGET.[InverterID] = @InverterID AND TARGET.[TimeStamp] = @TimeStamp
WHEN MATCHED THEN
UPDATE
SET [ValueA] = SOURCE.[ValueA], [ValueB] = SOURCE.[ValueB]
WHEN NOT MATCHED THEN
INSERT ([InverterID], [TimeStamp], [ValueA], [ValueB])
VALUES (SOURCE.[InverterID], SOURCE.[TimeStamp], SOURCE.[ValueA], SOURCE.[ValueB]);
END
Verwendung
Ich habe jetzt Dienstinstanzen auf mehreren Servern ausgeführt, die massive Aktualisierungen durchführen, indem sie die [InsertOrUpdateInverterData]
Prozedur schnell aufrufen .
Es gibt auch eine Website, die SELECT-Abfragen für die [InverterData]
Tabelle ausführt .
Problem
Wenn ich SELECT-Abfragen für die [InverterData]
Tabelle durchführe, werden sie in unterschiedlichen Zeiträumen abgearbeitet, abhängig von der INSERT-Verwendung meiner Dienstinstanzen. Wenn ich alle Dienstinstanzen pausiere, ist das SELECT blitzschnell, wenn die Instanz schnell einfügt, werden die SELECTs sehr langsam oder es kommt sogar zu einem Timeout-Abbruch.
Versuche
Ich habe einige SELECTs auf dem [sys.dm_tran_locks]
Tisch durchgeführt, um Sperrprozesse wie diesen zu finden
SELECT
tl.request_session_id,
wt.blocking_session_id,
OBJECT_NAME(p.OBJECT_ID) BlockedObjectName,
h1.TEXT AS RequestingText,
h2.TEXT AS BlockingText,
tl.request_mode
FROM sys.dm_tran_locks AS tl
INNER JOIN sys.dm_os_waiting_tasks AS wt ON tl.lock_owner_address = wt.resource_address
INNER JOIN sys.partitions AS p ON p.hobt_id = tl.resource_associated_entity_id
INNER JOIN sys.dm_exec_connections ec1 ON ec1.session_id = tl.request_session_id
INNER JOIN sys.dm_exec_connections ec2 ON ec2.session_id = wt.blocking_session_id
CROSS APPLY sys.dm_exec_sql_text(ec1.most_recent_sql_handle) AS h1
CROSS APPLY sys.dm_exec_sql_text(ec2.most_recent_sql_handle) AS h2
Das ist das Ergebnis:
S = geteilt. Die Wartesitzung erhält gemeinsamen Zugriff auf die Ressource.
Frage
Warum werden die SELECTs durch die [InsertOrUpdateInverterData]
Prozedur blockiert, die nur MERGE-Befehle verwendet?
Muss ich eine Transaktion mit definiertem Isolationsmodus innerhalb von verwenden [InsertOrUpdateInverterData]
?
Update 1 (im Zusammenhang mit der Frage von @Paul)
Basis für interne MS-SQL Server-Berichte zu [InsertOrUpdateInverterData]
folgenden Statistiken:
- Durchschnittliche CPU-Zeit: 0.12ms
- Durchschnittliche Lesevorgänge: 5,76 pro / s
- Durchschnittliche Schreibvorgänge: 0,4 pro / s
Auf dieser Basis sieht es so aus, als ob der Befehl MERGE hauptsächlich mit Leseoperationen beschäftigt ist, die die Tabelle sperren! (?)
Update 2 (im Zusammenhang mit der Frage von @Paul)
Die [InverterData]
Tabelle hat folgende Speicherstatistiken:
- Datenraum: 26.901,86 MB
- Zeilenanzahl: 131.827.749
- Aufgeteilt: wahr
- Anzahl der Partitionen: 62
Hier ist die (fast) vollständige Ergebnismenge von sp_WhoIsActive :
SELECT
Befehl
- TT HH: MM: SS: 00 00: 01: 01.930
- session_id: 73
- wait_info: (12629ms) LCK_M_S
- CPU: 198
- Blocking_Session_ID: 146
- liest: 99,368
- schreibt: 0
- Status: ausgesetzt
- open_tran_count: 0
[InsertOrUpdateInverterData]
Blockierbefehl
- TT HH: MM: SS: 00 00: 00: 00.330
- session_id: 146
- wait_info: NULL
- CPU: 3.972
- blocking_session_id: NULL
- liest: 376,95
- schreibt: 126
- Status: schlafen
- open_tran_count: 1
quelle
([TimeStamp] DESC, [InverterID] ASC)
sieht nach einer seltsamen Wahl für den Clustered-Index aus. Ich meine dasDESC
Teil.Antworten:
Erstens ist Ihre
MERGE
Aussage , obwohl sie etwas unabhängig von der Hauptfrage ist, potenziell einem Fehlerrisiko aufgrund eines Rennzustands ausgesetzt . Kurz gesagt, das Problem besteht darin, dass mehrere Threads gleichzeitig darauf schließen können, dass die Zielzeile nicht vorhanden ist, was zu Kollisionsversuchen beim Einfügen führt. Die Hauptursache ist, dass es nicht möglich ist, eine gemeinsame Sperre oder eine Aktualisierungssperre für eine Zeile zu übernehmen, die nicht vorhanden ist. Die Lösung besteht darin, einen Hinweis hinzuzufügen:Der Hinweis auf die serialisierbare Isolationsstufe stellt sicher, dass der Schlüsselbereich, in dem sich die Zeile befinden würde, gesperrt ist. Sie haben einen eindeutigen Index zur Unterstützung der Bereichssperrung, sodass dieser Hinweis die Sperrung nicht beeinträchtigt. Sie erhalten lediglich Schutz vor dieser potenziellen Race-Bedingung.
Hauptfrage
Bei der standardmäßigen Isolationsstufe für gesperrte Lesezugriffe werden beim Lesen von Daten gemeinsam genutzte (S-) Sperren verwendet und in der Regel (wenn auch nicht immer) unmittelbar nach Abschluss des Lesevorgangs freigegeben. Einige gemeinsam genutzte Sperren werden bis zum Ende der Anweisung beibehalten.
Eine
MERGE
Anweisung ändert Daten, sodass beim Auffinden der zu ändernden Daten S- oder Aktualisierungssperren (U) erfasst werden, die unmittelbar vor der eigentlichen Änderung in Exklusivsperren (X) konvertiert werden. Sowohl U- als auch X-Sperren müssen bis zum Ende der Transaktion gehalten werden.Dies gilt unter allen Isolationsstufen mit Ausnahme der ‚optimistisch‘ Snapshot - Isolation (SI) nicht - verwechselt werden mit Versionsverwaltung lesen verpflichtet, auch bekannt als read committed Snapshot - Isolation (RCSI).
Nichts in Ihrer Frage zeigt eine Sitzung, die darauf wartet, dass eine S-Sperre von einer Sitzung mit einer U-Sperre blockiert wird. Diese Schlösser sind kompatibel . Jede Blockierung wird mit ziemlicher Sicherheit durch das Blockieren eines gehaltenen X-Schlosses verursacht. Dies kann etwas schwierig zu erfassen sein, wenn eine große Anzahl von kurzfristigen Sperren in einem kurzen Zeitintervall erstellt, konvertiert und freigegeben wird.
Der
open_tran_count: 1
Befehl on the InsertOrUpdateInverterData ist eine Untersuchung wert. Obwohl der Befehl nicht sehr lange ausgeführt wurde, sollten Sie sicherstellen, dass keine unnötig lange Transaktion enthalten ist (in der Anwendung oder in der gespeicherten Prozedur auf höherer Ebene). Es wird empfohlen, Transaktionen so kurz wie möglich zu halten. Dies kann nichts sein, aber Sie sollten auf jeden Fall überprüfen.Mögliche Lösung
Wie Kin in einem Kommentar angedeutet hat, könnten Sie versuchen, eine Isolationsstufe für die Zeilenversionierung (RCSI oder SI) für diese Datenbank zu aktivieren . RCSI wird am häufigsten verwendet, da in der Regel nicht so viele Anwendungsänderungen erforderlich sind. Nach der Aktivierung verwendet die standardmäßige Isolationsstufe für festgeschriebene Lesevorgänge Zeilenversionen, anstatt S-Sperren für Lesevorgänge zu verwenden, sodass die SX-Blockierung reduziert oder beseitigt wird. Einige Operationen (z. B. Fremdschlüsselprüfungen) erhalten weiterhin S-Sperren unter RCSI.
Beachten Sie jedoch, dass Zeilenversionen Tempdb-Speicherplatz belegen, der im Großen und Ganzen proportional zur Änderungsrate und der Transaktionsdauer ist. Sie müssen Ihre Implementierung unter Last gründlich testen, um die Auswirkungen von RCSI (oder SI) in Ihrem Fall zu verstehen und zu planen.
Wenn Sie die Verwendung der Versionierung lokalisieren möchten, anstatt sie für den gesamten Workload zu aktivieren, ist SI möglicherweise die bessere Wahl. Wenn Sie SI für die Lesetransaktionen verwenden, vermeiden Sie Konflikte zwischen Lesern und Schreibern, da die Leser die Version der Zeile sehen müssen, bevor eine gleichzeitige Änderung gestartet wird die Zeile zum Zeitpunkt des Beginns der SI-Transaktion). Die Verwendung von SI für die Schreibtransaktionen ist wenig oder gar nicht vorteilhaft, da Schreibsperren weiterhin verwendet werden und Sie mit Schreibkonflikten umgehen müssen. Es sei denn, das ist was du willst :)
Hinweis: Im Gegensatz zu RCSI (das nach seiner Aktivierung für alle Transaktionen gilt, die mit festgeschriebenem Lesezugriff ausgeführt werden) muss SI explizit mit angefordert werden
SET TRANSACTION ISOLATION SNAPSHOT;
.Subtiles Verhalten, das davon abhängt, dass Leser (auch im Trigger-Code!) Schreiber blockieren, macht das Testen unerlässlich. Weitere Informationen finden Sie in meinen verlinkten Artikelserien und in der Onlinedokumentation. Wenn Sie sich für RCSI entscheiden, lesen Sie insbesondere die Datenänderungen unter Read Committed Snapshot Isolation .
Schließlich sollten Sie sicherstellen, dass Ihre Instanz auf SQL Server 2008 Service Pack 4 gepatcht ist.
quelle
Demütig würde ich keine Zusammenführung verwenden. Ich würde mit IF Exists (UPDATE) ELSE (INSERT) gehen - Sie haben einen gruppierten Schlüssel mit den zwei Spalten, die Sie zum Identifizieren der Zeilen verwenden, so dass es ein einfacher Test ist.
Sie erwähnen MASSIVE-Einfügungen und möchten die Daten 1 zu 1 in einer Staging-Tabelle stapeln und mit dem SQL-Datensatz POWER OVERWHELMING mehr als 1 Aktualisierung / Einfügung gleichzeitig ausführen ? Lassen Sie die Inhalte in der Staging-Tabelle routinemäßig testen und holen Sie sich jeweils die ersten 10000 statt jeweils 1 ...
Ich würde so etwas in meinem Update tun
Wahrscheinlich könnten Sie mehrere Jobs ausführen, um die Aktualisierungsstapel zu löschen, und Sie benötigen einen separaten Job, in dem ein Löschvorgang ausgeführt wird
um die Staging-Tabelle zu bereinigen.
quelle