Massive INSERTs blockieren SELECTs


14

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:

Bildbeschreibung hier eingeben

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

Das ([TimeStamp] DESC, [InverterID] ASC)sieht nach einer seltsamen Wahl für den Clustered-Index aus. Ich meine das DESCTeil.
ypercubeᵀᴹ

Ich verstehe Ihren Punkt: Clustered Index DESC, das Daten einfügt, würde die Tabellenwiederherstellung erzwingen und eher an das Ende anhängen ... Leistungshund; würde den Tisch sperren, während der Neuaufbau stattfindet ... ja. von Jove, du hast es. Die Struktur ist mehr Grund zum Sperren als zum Sperren.
Alocyte

Antworten:


12

Erstens ist Ihre MERGEAussage , 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:

MERGE [dbo].[InverterData] WITH (SERIALIZABLE) AS [TARGET]

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

Warum werden die SELECTsvon der [InsertOrUpdateInverterData] -Prozedur blockiert, die nur MERGEBefehle verwendet?

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 MERGEAnweisung ä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: 1Befehl 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.


0

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

DECLARE @Set TABLE (StagingKey, ID,DATE)
INSERT INTO @Set
UPDATE Staging 
SET InProgress = 1
OUTPUT StagingKey, Staging.ID, Staging.Date
WHERE InProgress = 0
AND StagingID IN (SELECT TOP (100000) StagingKey FROM Staging WHERE inProgress = 0 ORDER BY StagingKey ASC ) --FIFO

DECLARE @Temp 
INSERT INTO @TEMP 
UPDATE [DEST] SET Value = Staging.Value [whatever]
OUTPUT INSERTED.ID, DATE [row identifiers]
FROM [DEST] 
JOIN [STAGING]
JOIN [@SET]; 
INSERT INTO @TEMP 
INSERT [DEST] 
SELECT
OUTPUT INSERT.ID, DATE [row identifiers] 
FROM [STAGING] 
JOIN [@SET] 
LEFT JOIN [DEST]

UPDATE Staging
SET inProgress = NULL
FROM Staging 
JOIN @set
ON @Set.Key = Staging.Key
JOIN @temp
ON @temp.id = @set.ID
AND @temp.date = @set.Date

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

while exists (inProgress is null) 
delete top (100) from staging where inProgress is null 

um die Staging-Tabelle zu bereinigen.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.