Gibt es in SQL Server eine Möglichkeit zu überprüfen, ob eine ausgewählte Gruppe von Zeilen gesperrt ist oder nicht?


21

Wir versuchen, eine große Anzahl von Datensätzen in einer Multi-Milliarden-Zeilentabelle zu aktualisieren / löschen. Da dies ein beliebter Tisch ist, gibt es in verschiedenen Abschnitten dieses Tisches eine Menge Aktivität. Jede große Aktualisierungs- / Löschaktivität wird für längere Zeit blockiert (da darauf gewartet wird, dass alle Zeilen, Seitensperren oder Tabellensperren gesperrt werden), was zu Zeitüberschreitungen führt oder die Ausführung der Aufgabe mehrere Tage in Anspruch nimmt.

Daher ändern wir den Ansatz, um einen kleinen Stapel von Zeilen gleichzeitig zu löschen. Wir möchten jedoch prüfen, ob die ausgewählten (beispielsweise 100 oder 1000 oder 2000 Zeilen) derzeit durch einen anderen Prozess gesperrt sind oder nicht.

  • Wenn nicht, fahren Sie mit Löschen / Aktualisieren fort.
  • Wenn sie gesperrt sind, fahren Sie mit der nächsten Gruppe von Datensätzen fort.
  • Kehren Sie am Ende zum Anfang zurück und versuchen Sie, die ausgelassenen zu aktualisieren / löschen.

Ist das machbar?

Vielen Dank, ToC


2
Haben Sie sich READPAST als Teil der delete-Anweisung oder NOWAIT angesehen (um die gesamte Gruppe zu scheitern)? Eine davon könnte für Sie arbeiten. msdn.microsoft.com/en-us/library/ms187373.aspx
Sean sagt Entfernen Sara Chipps

@ SeanGallardy Ich habe diese Idee nicht berücksichtigt, aber jetzt werde ich. Aber gibt es eine einfachere Möglichkeit zu überprüfen, ob eine bestimmte Zeile gesperrt ist oder nicht? Vielen Dank.
ToC

3
Sie können auch nach LOCK_TIMEOUT ( msdn.microsoft.com/en-us/library/ms189470.aspx ) suchen . Auf diese Weise stellt beispielsweise Adam Machanics sp_whoisactive sicher, dass die Prozedur nicht zu lange wartet, wenn sie beim Versuch, einen Ausführungsplan zu erstellen, blockiert wird. Sie können ein kurzes Zeitlimit festlegen oder sogar den Wert 0 verwenden ("0 bedeutet, dass Sie nicht warten und eine Nachricht zurückgeben, sobald eine Sperre festgestellt wird."). Sie können dies mit einem TRY / CATCH-Befehl kombinieren, um den Fehler 1222 ( "Zeitüberschreitung der Sperranforderung") und fahren Sie mit der nächsten Charge fort.
Geoff Patterson

Interessanter Ansatz. Ich werde das auch versuchen.
ToC

2
Um zu antworten: Nein, es gibt keine einfachere Möglichkeit, festzustellen, ob die Zeilen gesperrt sind, es sei denn, in der Anwendung wird speziell etwas unternommen. Grundsätzlich könnte man zuerst mit HOLDLOCK und XLOCK eine Auswahl mit einem lock_timeout-Satz treffen (worum es in meinem ursprünglichen Kommentar bei NOWAIT geht, indem man das Timeout auf 0 setzt). Wenn Sie es nicht bekommen, dann wissen Sie, dass etwas gesperrt ist. Es ist nicht einfach zu sagen, "Ist Zeile X in Tabelle Y mit Index Z durch etwas gesperrt". Wir können sehen, ob die Tabelle Sperren hat oder ob Seiten / Zeilen / Schlüssel / usw. Sperren haben, aber das in bestimmte Zeilen in einer Abfrage zu übersetzen, wäre nicht einfach.
Sean sagt Entfernen Sara Chipps

Antworten:


10

Wenn ich die Anforderung richtig verstehe, besteht das Ziel darin, Stapel von Zeilen zu löschen, während gleichzeitig DML-Vorgänge für Zeilen in der gesamten Tabelle ausgeführt werden. Das Ziel ist das Löschen eines Stapels. Wenn jedoch zugrunde liegende Zeilen in dem von diesem Stapel definierten Bereich gesperrt sind, müssen wir diesen Stapel überspringen und zum nächsten Stapel übergehen. Wir müssen dann zu allen Stapeln zurückkehren, die zuvor nicht gelöscht wurden, und unsere ursprüngliche Löschlogik wiederholen. Wir müssen diesen Zyklus wiederholen, bis alle erforderlichen Zeilenstapel gelöscht sind.

Wie bereits erwähnt, ist es sinnvoll, einen READPAST-Hinweis und die Isolationsstufe READ COMMITTED (Standard) zu verwenden, um vergangene Bereiche zu überspringen, die möglicherweise blockierte Zeilen enthalten. Ich werde noch einen Schritt weiter gehen und empfehlen, die Isolationsstufe SERIALISIERBAR zu verwenden und Löschvorgänge zu knabbern.

SQL Server verwendet Schlüsselbereichssperren, um einen Bereich von Zeilen zu schützen, die implizit in einem Datensatz enthalten sind, der von einer Transact-SQL-Anweisung gelesen wird, während die serialisierbare Transaktionsisolationsstufe verwendet wird. Weitere Informationen finden Sie hier: https://technet.microsoft.com /en-US/library/ms191272(v=SQL.105).aspx

Beim Löschen von Nibbeln ist es unser Ziel, einen Bereich von Zeilen zu isolieren und sicherzustellen, dass keine Änderungen an diesen Zeilen vorgenommen werden, während sie gelöscht werden. Das heißt, wir möchten keine Phantom-Lesevorgänge oder Einfügungen. Die serialisierbare Isolationsstufe soll dieses Problem lösen.

Bevor ich meine Lösung demonstriere, möchte ich hinzufügen, dass ich weder empfehle, die Standardisolationsstufe Ihrer Datenbank auf SERIALIZABLE zu ändern, noch empfehle ich, dass meine Lösung die beste ist. Ich möchte es nur vorstellen und sehen, wo wir von hier aus hingehen können.

Ein paar haushaltsnotizen:

  1. Die von mir verwendete SQL Server-Version ist Microsoft SQL Server 2012 - 11.0.5343.0 (X64).
  2. Meine Testdatenbank verwendet das vollständige Wiederherstellungsmodell

Zu Beginn meines Experiments werde ich eine Testdatenbank und eine Beispieltabelle einrichten und die Tabelle mit 2.000.000 Zeilen füllen.


USE [master];
GO

SET NOCOUNT ON;

IF DATABASEPROPERTYEX (N'test', N'Version') > 0
BEGIN
    ALTER DATABASE [test] SET SINGLE_USER
        WITH ROLLBACK IMMEDIATE;
    DROP DATABASE [test];
END
GO

-- Create the test database
CREATE DATABASE [test];
GO

-- Set the recovery model to FULL
ALTER DATABASE [test] SET RECOVERY FULL;

-- Create a FULL database backup
-- in order to ensure we are in fact using 
-- the FULL recovery model
-- I pipe it to dev null for simplicity
BACKUP DATABASE [test]
TO DISK = N'nul';
GO

USE [test];
GO

-- Create our table
IF OBJECT_ID('dbo.tbl','U') IS NOT NULL
BEGIN
    DROP TABLE dbo.tbl;
END;
CREATE TABLE dbo.tbl
(
      c1 BIGINT IDENTITY (1,1) NOT NULL
    , c2 INT NOT NULL
) ON [PRIMARY];
GO

-- Insert 2,000,000 rows 
INSERT INTO dbo.tbl
    SELECT TOP 2000
        number
    FROM
        master..spt_values
    ORDER BY 
        number
GO 1000

Zu diesem Zeitpunkt benötigen wir einen oder mehrere Indizes, auf die die Sperrmechanismen der Isolationsstufe SERIALIZABLE einwirken können.


-- Add a clustered index
CREATE UNIQUE CLUSTERED INDEX CIX_tbl_c1
    ON dbo.tbl (c1);
GO

-- Add a non-clustered index
CREATE NONCLUSTERED INDEX IX_tbl_c2 
    ON dbo.tbl (c2);
GO

Überprüfen wir nun, ob unsere 2.000.000 Zeilen erstellt wurden


SELECT
    COUNT(*)
FROM
    tbl;

Bildbeschreibung hier eingeben

Wir haben also unsere Datenbank, Tabelle, Indizes und Zeilen. Lassen Sie uns also das Experiment zum Knabbern von Löschvorgängen einrichten. Zunächst müssen wir entscheiden, wie ein typischer Knabber-Löschmechanismus am besten erstellt werden soll.


DECLARE
      @BatchSize        INT    = 100
    , @LowestValue      BIGINT = 20000
    , @HighestValue     BIGINT = 20010
    , @DeletedRowsCount BIGINT = 0
    , @RowCount         BIGINT = 1;

SET NOCOUNT ON;
GO

WHILE  @DeletedRowsCount <  ( @HighestValue - @LowestValue ) 
BEGIN

    SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
    BEGIN TRANSACTION

        DELETE 
        FROM
            dbo.tbl 
        WHERE
            c1 IN ( 
                    SELECT TOP (@BatchSize)
                        c1
                    FROM
                        dbo.tbl 
                    WHERE 
                        c1 BETWEEN @LowestValue AND @HighestValue
                    ORDER BY 
                        c1
                  );

        SET @RowCount = ROWCOUNT_BIG();

    COMMIT TRANSACTION;

    SET @DeletedRowsCount += @RowCount;
    WAITFOR DELAY '000:00:00.025';
    CHECKPOINT;

END;

Wie Sie sehen, habe ich die explizite Transaktion in die while-Schleife eingefügt. Wenn Sie das Löschen von Protokollen begrenzen möchten, können Sie es auch außerhalb der Schleife platzieren. Da wir uns im vollständigen Wiederherstellungsmodell befinden, möchten Sie möglicherweise häufiger Transaktionsprotokollsicherungen erstellen, während Sie die Löschvorgänge für das Nibbeln ausführen, um sicherzustellen, dass Ihr Transaktionsprotokoll nicht übermäßig anwächst.

Also, ich habe ein paar Ziele mit diesem Setup. Erstens möchte ich meine Tastensperren; Deshalb versuche ich, die Chargen so klein wie möglich zu halten. Ich möchte auch die Nebenläufigkeit auf meinem "gigantischen" Tisch nicht negativ beeinflussen. Also möchte ich meine Schlösser nehmen und sie so schnell wie möglich verlassen. Daher empfehle ich, dass Sie die Losgrößen klein halten.

Nun möchte ich ein sehr kurzes Beispiel für diese Löschroutine in Aktion geben. Wir müssen ein neues Fenster in SSMS öffnen und eine Zeile aus unserer Tabelle löschen. Ich werde dies innerhalb einer impliziten Transaktion mit der Standardisolationsstufe READ COMMITTED tun.


DELETE FROM
    dbo.tbl
WHERE
    c1 = 20005;

Wurde diese Zeile tatsächlich gelöscht?


SELECT
    c1
FROM
    dbo.tbl
WHERE
    c1 BETWEEN 20000 AND 20010;

Ja, es wurde gelöscht.

Beweis der gelöschten Zeile

Um unsere Sperren anzuzeigen, öffnen wir jetzt ein neues Fenster in SSMS und fügen ein oder zwei Codefragmente hinzu. Ich verwende Adam Mechanics sp_whoisactive, das hier zu finden ist: sp_whoisactive


SELECT
    DB_NAME(resource_database_id) AS DatabaseName
  , resource_type
  , request_mode
FROM
    sys.dm_tran_locks
WHERE
    DB_NAME(resource_database_id) = 'test'
    AND resource_type = 'KEY'
ORDER BY
    request_mode;

-- Our insert
sp_lock 55;

-- Our deletions
sp_lock 52;

-- Our active sessions
sp_whoisactive;

Nun sind wir bereit zu beginnen. Beginnen wir in einem neuen SSMS-Fenster eine explizite Transaktion, die versucht, die eine gelöschte Zeile erneut einzufügen. Zur gleichen Zeit werden wir unsere Knabber-Löschoperation auslösen.

Der Einfügungscode:


BEGIN TRANSACTION

    SET IDENTITY_INSERT dbo.tbl ON;

    INSERT  INTO dbo.tbl
            ( c1 , c2 )
    VALUES
            ( 20005 , 1 );

    SET IDENTITY_INSERT dbo.tbl OFF;

--COMMIT TRANSACTION;

Beginnen wir beide Operationen mit der Einfügung und führen dann die Löschvorgänge aus. Wir können die Schlüsselbereichsschlösser und exklusiven Schlösser sehen.

Reichweite und exklusive Schlösser

Die Einfügung erzeugte diese Sperren:

Insert's Locks

Das knabbernde Löschen / Auswählen hält diese Sperren:

Bildbeschreibung hier eingeben

Unser Insert blockiert das Löschen wie erwartet:

Blöcke einfügen Löschen

Lassen Sie uns nun die Insert-Transaktion festschreiben und sehen, was los ist.

Bestätigen Sie das Löschen

Und wie erwartet sind alle Transaktionen abgeschlossen. Nun müssen wir prüfen, ob es sich bei der Einfügung um ein Phantom handelte oder ob die Löschoperation es ebenfalls entfernt hat.


SELECT
    c1
FROM
    dbo.tbl
WHERE
    c1 BETWEEN 20000 AND 20015;

Tatsächlich wurde die Einfügung gelöscht. Daher war keine Phantomeinfügung zulässig.

Kein Phantomeinsatz

Abschließend möchte ich sagen, dass die wahre Absicht dieser Übung nicht darin besteht, jede einzelne Sperre auf Zeilen-, Seiten- oder Tabellenebene nachzuverfolgen und zu ermitteln, ob ein Element eines Stapels gesperrt ist und daher unsere Löschoperation erforderlich machen würde warten. Das mag die Absicht der Fragesteller gewesen sein; Diese Aufgabe ist jedoch herkulisch und im Grunde unpraktisch, wenn nicht unmöglich. Das eigentliche Ziel ist es, sicherzustellen, dass keine unerwünschten Phänomene auftreten, sobald wir den Bereich unserer Charge mit eigenen Sperren isoliert und die Charge anschließend gelöscht haben. Die Isolationsstufe SERIALIZABLE erreicht dieses Ziel. Der Schlüssel ist, Ihre Knabbereien klein zu halten, Ihr Transaktionsprotokoll unter Kontrolle zu halten und unerwünschte Phänomene zu beseitigen.

Wenn Sie Geschwindigkeit wünschen, sollten Sie keine riesigen Tabellen erstellen, die nicht partitioniert werden können, und daher Partitionswechsel nicht verwenden, um die schnellsten Ergebnisse zu erzielen. Der Schlüssel zur Geschwindigkeit liegt in Partitionierung und Parallelität. Der Schlüssel zum Leiden sind Knabbereien und Live-Locking.

Bitte sag mir was du denkst.

Ich habe einige weitere Beispiele für die Isolationsstufe SERIALIZABLE in Aktion erstellt. Sie sollten unter den folgenden Links verfügbar sein.

Vorgang löschen

Operation einfügen

Gleichstellungsoperationen - Tastensperre für die nächsten Schlüsselwerte

Gleichstellungsoperationen - Singleton-Abruf vorhandener Daten

Gleichstellungsoperationen - Singleton-Abruf nicht vorhandener Daten

Ungleichungsoperationen - Schlüsselbereich sperrt den Bereich und die nächsten Schlüsselwerte


9

Daher ändern wir den Ansatz, um einen kleinen Stapel von Zeilen gleichzeitig zu löschen.

Dies ist eine wirklich gute Idee, um in kleinen, sorgfältigen Mengen oder Stücken zu löschen . Ich würde ein kleines und je nach Wiederherstellungsmodell der Datenbank - wenn , dann ein und wenn, dann ein , um ein Aufblähen des Transaktionsprotokolls zu vermeiden - zwischen den Stapeln hinzufügen .waitfor delay '00:00:05'FULLlog backupSIMPLEmanual CHECKPOINT

Wir möchten jedoch prüfen, ob die ausgewählten (beispielsweise 100 oder 1000 oder 2000 Zeilen) derzeit durch einen anderen Prozess gesperrt sind oder nicht.

Was Sie sagen, ist nicht ohne weiteres möglich (beachten Sie Ihre 3 Aufzählungszeichen). Wenn der obige Vorschlag - small batches + waitfor delaynicht funktioniert (vorausgesetzt, Sie führen ordnungsgemäße Tests durch), können Sie den verwenden query HINT.

Nicht verwenden NOLOCK- siehe kb / 308886 , SQL Server- Lesekonsistenzprobleme von Itzik Ben-Gan , NOLOCK überall einsetzen - Von Aaron Bertrand und SQL Server NOLOCK Hinweis und andere schlechte Ideen .

READPASTHinweis wird in Ihrem Szenario helfen. Der Kern des READPASTHinweises ist: Wenn es eine Sperre auf Zeilenebene gibt, wird sie von SQL Server nicht gelesen.

Gibt an, dass das Datenbankmodul keine Zeilen liest, die von anderen Transaktionen gesperrt wurden. Wenn READPASTangegeben, werden Sperren auf Zeilenebene übersprungen. Das heißt, das Datenbankmodul überspringt die Zeilen, anstatt die aktuelle Transaktion zu blockieren, bis die Sperren aufgehoben werden.

Während meiner begrenzten Tests fand ich wirklich gut Durchsatz bei der Verwendung DELETE from schema.tableName with (READPAST, READCOMMITTEDLOCK)und Einstellung der Abfragesitzung Isolationsstufe READ COMMITTEDverwenden , SET TRANSACTION ISOLATION LEVEL READ COMMITTEDdie ohnehin Standardisolationsstufe ist.


2

Zusammenfassung anderer Ansätze, die ursprünglich in Kommentaren zur Frage angeboten wurden.


  1. Verwenden Sie NOWAITdiese Option, wenn das gewünschte Verhalten darin besteht, den gesamten Block zu schließen, sobald eine inkompatible Sperre festgestellt wird.

    Aus der NOWAITDokumentation :

    Weist das Datenbankmodul an, eine Nachricht zurückzugeben, sobald eine Sperre für die Tabelle festgestellt wird. NOWAITentspricht der Angabe SET LOCK_TIMEOUT 0für eine bestimmte Tabelle. Der NOWAITHinweis funktioniert nicht, wenn der TABLOCKHinweis ebenfalls enthalten ist. Um eine Abfrage zu beenden, ohne zu warten, während der TABLOCKHinweis verwendet wird, stellen Sie der Abfrage SETLOCK_TIMEOUT 0;stattdessen Folgendes voran.

  2. Verwenden Sie SET LOCK_TIMEOUTdiese Option , um ein ähnliches Ergebnis zu erzielen, jedoch mit einem konfigurierbaren Timeout:

    Aus der SET LOCK_TIMEOUTDokumentation

    Gibt die Anzahl der Millisekunden an, die eine Anweisung auf die Freigabe einer Sperre wartet.

    Wenn das Warten auf eine Sperre den Zeitüberschreitungswert überschreitet, wird ein Fehler zurückgegeben. Der Wert 0 bedeutet, dass Sie nicht warten und eine Nachricht zurückgeben müssen, sobald eine Sperre festgestellt wird.


0

Angenommen, wir haben zwei Parallelen-Abfragen:

connect / session 1: sperrt die Zeile = 777

SELECT * FROM your_table WITH(UPDLOCK,READPAST) WHERE id = 777

connect / session 2: ignoriert die gesperrte Zeile = 777

SELECT * FROM your_table WITH(UPDLOCK,READPAST) WHERE id = 777

ODER connect / session 2: Löst eine Ausnahme aus

DECLARE @id integer;
SELECT @id = id FROM your_table WITH(UPDLOCK,READPAST) WHERE id = 777;
IF @id is NULL
  THROW 51000, 'Hi, a record is locked or does not exist.', 1;

-1

Versuchen Sie, nach so etwas zu filtern - es kann kompliziert werden, wenn Sie wirklich, wirklich spezifisch werden möchten. Suchen Sie in BOL nach der Beschreibung von sys.dm_tran_locks

SELECT 
tl.request_session_id,
tl.resource_type,
tl.resource_associated_entity_id,
db_name(tl.resource_database_id) 'Database',
CASE 
    WHEN tl.resource_type = 'object' THEN object_name(tl.resource_associated_entity_id, tl.resource_database_id)
    ELSE NULL
END 'LockedObject',
tl.resource_database_id,
tl.resource_description,
tl.request_mode,
tl.request_type,
tl.request_status FROM [sys].[dm_tran_locks] tl WHERE resource_database_id <> 2order by tl.request_session_id

nur neugierig - warum die downvote?
Rottengeek

-11

Sie können NoLOCK verwenden, während Sie löschen. Wenn die Zeilen gesperrt sind, werden sie nicht gelöscht. Es ist nicht ideal, kann aber den Trick für Sie tun.

DELETE TA FROM dbo.TableA TA WITH (NOLOCK) WHERE Condition = True

7
Wenn ich versuche , dass auf meinem lokalen Rechner erhalte ich Msg 1065, Level 15, State 1, Line 15 The NOLOCK and READUNCOMMITTED lock hints are not allowed for target tables of INSERT, UPDATE, DELETE or MERGE statements., seit 2005 als veraltet
Tom V - Team Monica
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.